VektRPC开发日志三:过滤器、优化与SpringStarter开发

Version3 过滤器、优化与SpringStarter开发

SPI

SPI 机制是JDK提供的一个本地服务注册与加载的机制.

这么讲可能有点抽象,

image.png

首先,一个接口可能会有多个实现类。如果按照一般的方式,我们可能会在程序运行的时候,根据某些条件来判断使用哪一个实现类。

但是,这样有一个缺点就是不方便我们后续拓展。而SPI 多用于框架扩展、插件开发等等。

如何使用SPI

首先需要有个 interface

1
2
3
public interface test{
public void hello();
}

然后我们实现一些这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//StudentA.java
public class StudentA implements test{
@Override
public void hello(){
sout("I'm a good boy!");
}
}

//StudentB.java
public class StudentB implements test{
@Override
public void hello(){
sout("都多余了, 是不是好脸给你给多了");
}
}

以上就是我们自己实现的两个类。
(下图只是举例子)
image.png

在resource/META-INF/service 中有我们以接口名的引用为文件名的文件,内容是我们想要调用的实现类。(假如内容是StudentB的引用)

我们在代码里实现:

1
2
3
4
5
6
7
8
public static void main(String[] args) {  
ServiceLoader<test> serviceLoader = ServiceLoader.load(test.class);
Iterator<test> iSpiTestIterator = serviceLoader.iterator();
while (iSpiTestIterator.hasNext()) {
ISpiTest iSpiTest = iSpiTestIterator.next();
TestSpiDemo.doTest(iSpiTest);
}
}

打印的内容:

1
都多余了, 是不是好脸给你给多了

有谁在用SPI机制呢, 网上大多数都会说JDBC之类的,那我就讲一个更为简单。就是我们常常用的log4j.

image.png

如何为我们所用?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ExtensionLoader {
public static String EXTENSION_LOADER_DIR_PREFIX = "META-INF/vektrpc/";


public static Map<String, LinkedHashMap<String, Class>> EXTENSION_LOADER_CLASS_CACHE = new ConcurrentHashMap<>();

public void loadExtension(Class clazz) throws IOException, ClassNotFoundException {
if(clazz == null){
throw new IllegalArgumentException("class can not null");
}

String spiPath = EXTENSION_LOADER_DIR_PREFIX + clazz.getName();
ClassLoader classLoader = this.getClass().getClassLoader();
Enumeration<URL> enumeration = classLoader.getResources(spiPath);
while(enumeration.hasMoreElements()){
URL url = enumeration.nextElement();
InputStreamReader inputStreamReader = null;
inputStreamReader = new InputStreamReader(url.openStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String line = null;
LinkedHashMap<String, Class> classMap = new LinkedHashMap<>();
while((line = bufferedReader.readLine()) != null){
//如果配置中加入了#开头,则表示忽略该类,无需加载
if (line.startsWith("#")){
continue;
}
String[] lineArr = line.split("=");
String implClassName = lineArr[0];
String interfaceName = lineArr[1];
//保存的同时初始化类
classMap.put(implClassName,Class.forName(interfaceName));
}
//放入缓存中
if (EXTENSION_LOADER_CLASS_CACHE.containsKey(clazz.getName())){
EXTENSION_LOADER_CLASS_CACHE.get(clazz.getName()).putAll(classMap);
}else {
EXTENSION_LOADER_CLASS_CACHE.put(clazz.getName(),classMap);
}
}


}
}

我们首先定义了SPI配置文件所在的文件夹在 MATE-INF/service ,然后当我们传入一个 interface的类。我们的 ExtensionLoader 就会去指定目录下去找配置文件,然后读入实现类放入EXTENSION_LOADER_CLASS_CACHE, 等需要是从中取得Class 然后反射new一个。

过滤器设计

image.png

它只是一个壳子

1
2
3
4
5
6
7
8
9
10
public interface IFilter {
}

public interface IServerFilter extends IFilter {
void doFilter(RpcInvocation rpcInvocation);
}

public interface IClientFilter extends IFilter {
void doFilter(RpcInvocation rpcInvocation);
}

怎么用捏,不想写了。

1
2
3
4
5
6
7
8
9
@SPI("before")  
public class ServerLogFilterImpl implements IServerFilter{

private static final Logger LOGGER = LoggerFactory.getLogger(ServerLogFilterImpl.class);
@Override
public void doFilter(RpcInvocation rpcInvocation) {
LOGGER.info(rpcInvocation.getAttachments().get("c_app_name") + "do invoke ------>" + rpcInvocation.getTargetServiceName() + "#" + rpcInvocation.getTargetMethod());
}
}

我们以上面为例子,这是一个打印日志的过滤器。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ServerFilterChain {
private static List<IServerFilter> iServerFilters = new ArrayList<>();

public void addServerFilter(IServerFilter serverFilter){
iServerFilters.add(serverFilter);
}

public void doFilter(RpcInvocation rpcInvocation){
for(IServerFilter iServerFilter : iServerFilters){
iServerFilter.doFilter(rpcInvocation);
}
}
}

打印完了之后,这个就过滤器就结束了,而上面的 @SPI ,是用来表示这个是前过滤器还是后过滤器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void initServerConfig() throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException {
SERVER_CONFIG = PropertiesBootstrap.loadServerConfigFromLocal();
//初始化线程池和队列配置
SERVER_CHANNEL_DISPATCHER.init(SERVER_CONFIG.getServerQueueSize(), SERVER_CONFIG.getServerBizThreadNums());
//初始化序列化方式
EXTENSION_LOADER.loadExtension(SerializeFactory.class);
String serializeType = SERVER_CONFIG.getServerSerializeType();
LinkedHashMap<String, Class> serializeTypeMap = EXTENSION_LOADER_CLASS_CACHE.get(SerializeFactory.class.getName());
Class serializeClass = serializeTypeMap.get(serializeType);
if (serializeClass == null) {
throw new RuntimeException("no match serialize type for " + serializeType);
}
SERVER_SERIALIZE_FACTORY = (SerializeFactory) serializeClass.newInstance();

//初始化过滤链
EXTENSION_LOADER.loadExtension(IServerFilter.class);
ServerFilterChain serverBeforeFilterChain = new ServerFilterChain();
ServerFilterChain serverAfterFilterChain = new ServerFilterChain();
LinkedHashMap<String, Class> filterMap = EXTENSION_LOADER_CLASS_CACHE.get(IServerFilter.class.getName());
for (String implClassName : filterMap.keySet()) {
Class filterClass = filterMap.get(implClassName);
if (filterClass == null) {
throw new NullPointerException("no match server filter for " + implClassName);
}
Annotation spiAnnotation = filterClass.getDeclaredAnnotation(SPI.class);
if (spiAnnotation == null) {
logger.warn("filter {}spi annotation is null ", filterClass.getName());
continue;
}
SPI spi = (SPI) spiAnnotation;
if("before".equals(spi.value())){
serverBeforeFilterChain.addServerFilter((IServerFilter) filterClass.newInstance());
}else if("after".equals(spi.value())){
serverAfterFilterChain.addServerFilter((IServerFilter) filterClass.newInstance());
}

}
SERVER_BEFORE_FILTER_CHAIN = serverBeforeFilterChain;
SERVER_AFTER_FILTER_CHAIN = serverAfterFilterChain;
}

在上面我们统一设置过滤器链。

并发优化

我们之前的rpc还是有点太Naive了。

在这个基础上我们增加了 ServerChannelReadData和 ServerChannelDispatcher

1
2
3
4
5
6
public class ServerChannelReadData {

private RpcProtocol rpcProtocol;

private ChannelHandlerContext channelHandler;
}

Data就是主要来存放我们收取到的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class ServerChannelDispatcher {
private static final Logger logger = LoggerFactory.getLogger(ServerChannelDispatcher.class);
private BlockingQueue<ServerChannelReadData> RPC_DATA_QUEUE;

private ExecutorService executorService;

public void init(int queueSize, int bizThreadNum){
RPC_DATA_QUEUE = new ArrayBlockingQueue<>(queueSize);
executorService = new ThreadPoolExecutor(bizThreadNum, bizThreadNum, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(512));

}

public void add(ServerChannelReadData serverChannelReadData){
RPC_DATA_QUEUE.add(serverChannelReadData);
}

public ServerChannelDispatcher(){

}

class ServerJobCoreHandle implements Runnable{
@Override
public void run() {
while(true){
try{
ServerChannelReadData serverChannelReadData = RPC_DATA_QUEUE.take();
logger.info("msg 收到");
executorService.submit(() -> {
RpcProtocol rpcProtocol = serverChannelReadData.getRpcProtocol();
RpcInvocation rpcInvocation = SERVER_SERIALIZE_FACTORY.deserialize(rpcProtocol.getContent(), RpcInvocation.class);
logger.info(JSONObject.toJSONString(rpcInvocation));
//doFilter
try {
//doBeforeFilter 前置过滤器
SERVER_BEFORE_FILTER_CHAIN.doFilter(rpcInvocation);
} catch (Exception e) {
//针对自定义异常进行处理
if (e instanceof VektRpcException){
logger.error("ServerChannelDispatcher#serverJobCore: filter error", e);
VektRpcException rpcException = (VektRpcException) e;
RpcInvocation reqParam = rpcException.getRpcInvocation();

byte[] body = SERVER_SERIALIZE_FACTORY.serialize(reqParam);
RpcProtocol respRpcProtocol = new RpcProtocol(body);
serverChannelReadData.getChannelHandler().writeAndFlush(respRpcProtocol);
return;
}
}


//PROVIDER_CLASS_MAP就是一开始预先在启动的时候存储的Bean集合
Object aimObject = PROVIDER_CLASS_MAP.get(rpcInvocation.getTargetServiceName());
Method[] methods = aimObject.getClass().getDeclaredMethods();
Object result = null;
for(Method method : methods){
if(method.getName().equals(rpcInvocation.getTargetMethod())){
if(method.getReturnType().equals(Void.TYPE)){
try {
method.invoke(aimObject, rpcInvocation.getArgs());
}catch (Exception e){
logger.error("ServerChannelDispatcher#serverJobCore: call error", e);
rpcInvocation.setE(e);
}

}else{
try{
result = method.invoke(aimObject, rpcInvocation.getArgs());
}catch (Exception e){
logger.error("ServerChannelDispatcher#serverJobCore: call error", e);
rpcInvocation.setE(e);
}

}
break;

}
}
boolean isAsync = (boolean) rpcInvocation.getAttachments().get("async");
if (isAsync){
//如果是异步请求则不用返回结结果,减少网络传输
return;
}

rpcInvocation.setResponse(result);
//doAfterFilter 后置处理器
SERVER_AFTER_FILTER_CHAIN.doFilter(rpcInvocation);
RpcProtocol respRpcProtocol = new RpcProtocol(SERVER_SERIALIZE_FACTORY.serialize(rpcInvocation));
serverChannelReadData.getChannelHandler().writeAndFlush(respRpcProtocol);
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}

public void startDataConsume(){
Thread thread = new Thread(new ServerJobCoreHandle());
thread.start();
}

}

我们会将Provider端的异常回填给Consumer, 以免服务端报错了都还不知到。

同时我们也是利用线程实时的去拿收取到的信息。

后面的懒得写了哎,好麻烦。

VektRPC开发日志二:对于Nacos的引入以及Router设计

Version2 对于Nacos的引入以及Router设计

Nacos 服务注册中心

由于本 Vekt RPC 目前是主要针对于Nacos的一款RPC, 因此首先引入 NacosClient.

Nacos 可以分为 ConfigServiceNamingService ,主要就是配置中心与服务注册中心。
我们主要是会用到NamingService.

1
2
3
4
5
<dependency>  
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.2.4</version>
</dependency>

我们先来看一下我们项目的图:

RegistryService 作为注册服务接口,给出了注册、反注册、订阅、取消订阅的方法,而后被 AbstractRegister 默认实现,大部分参数是URL,也就是我们的Instance 的一些信息。将其在Cache中移动。

同时我们也规范了 NacosClient 的一些方法在 AbstractNacosClient 由 NacosClient 做具体的方法实现。

最后我们得到了NacosRegister继承AbstractRegister 与 实现RegsitryService. 通过在内部实例化化一个NacosClient实例,从而与Nacos进行通信获取一些Service下的 instance。

Cache的改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//AbstractRegistry中的订阅抽象方法, 这个就是订阅的List
public static List<URL> SUBSCRIBE_SERVICE_LIST = new CopyOnWriteArrayList<URL>();

//每个服务的订阅的监听器,我们将其存放在Map中
public static Map<String, EventListener> SERVICE_LISTENER = new ConcurrentHashMap<>();


//对于服务的Provider 以及对应的权重
public static Map<String, Map<String, Double>> URL_MAP = new ConcurrentHashMap<>();

//IP:PORT
public static Set<String> SERVER_ADDRESS = new HashSet<>();

//每次进行远程调用的时候都是从这里面去选择服务提供者
public static Map<String,List<ChannelFutureWrapper>> CONNECT_MAP = new ConcurrentHashMap<>();

//随机请求的Map,key:serviceName,value:provider channel列表
public static Map<String, ChannelFutureWrapper[]> SERVICE_ROUTER_MAP = new ConcurrentHashMap<>();

//每个实例的ChannelFutureWrapper的轮询抽选封装
public static ChannelFuturePollingRef CHANNEL_FUTURE_POLLING_REF = new ChannelFuturePollingRef();

//路由
public static VektRouter VEKT_ROUTER;

Router的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface VektRouter {

/**
* 刷新路由数组
* @param selector
*/
void refreshRouterArr(Selector selector);

/**
* 对应 Provider 连接通道
* @param selector
* @return
*/
ChannelFutureWrapper select(Selector selector);

void updateWeight(URL url);
}

目前路由组件主要涉及到两个方面:轮询与随机路由。Selector只包含了服务名称。这三个部分组成了路由的核心功能。

两种路由分别实现的类是:RotateRouterImpl RandomRouterImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class RotateRouterImpl implements VektRouter{
@Override
public void refreshRouterArr(Selector selector) {
List<ChannelFutureWrapper> channelFutureWrappers = CONNECT_MAP.get(selector.getProviderServiceName());
ChannelFutureWrapper[] arr = new ChannelFutureWrapper[channelFutureWrappers.size()];

//提权生成调用先后顺序的数组

for(int i = 0; i < arr.length; i++){
arr[i] = channelFutureWrappers.get(i);
}
SERVICE_ROUTER_MAP.put(selector.getProviderServiceName(), arr);
}

@Override
public ChannelFutureWrapper select(Selector selector) {
return CHANNEL_FUTURE_POLLING_REF.getChannelFutureWrap(selector.getProviderServiceName());
}

@Override
public void updateWeight(URL url) {

}
}

其上面的select的方法,就是从各个连接中取得一个channelFuture, 但是这个步骤封装在了ChanneFuturePollingRef中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ChannelFuturePollingRef {

private AtomicLong referenceTimes = new AtomicLong(0);

public ChannelFutureWrapper getChannelFutureWrap(String serviceName){
ChannelFutureWrapper[] channelFutureWrappers = SERVICE_ROUTER_MAP.get(serviceName);

//自增取余数 顺序便利
long i = referenceTimes.getAndIncrement();
int index = (int) (i % channelFutureWrappers.length);
return channelFutureWrappers[index];
}

}

因此实现顺序和乱序的区别就应该是我们以何种顺序将各个连接放进 SERVICE_ROUTER_MAP 中。
如此我们就可以通过顺序访问 SERVICE_ROUTER_MAP 中的 key 数组就可以实现随机或轮询使用。

从上面得知,随机就是把连接的顺序打乱之后再放入,所以实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class RandomRouterImpl implements VektRouter{
@Override
public void refreshRouterArr(Selector selector) {
List<ChannelFutureWrapper> channelFutureWrappers = CONNECT_MAP.get(selector.getProviderServiceName());
ChannelFutureWrapper[] arr = new ChannelFutureWrapper[channelFutureWrappers.size()];
//提权生成调用先后顺序的随机数组
int[] result = createRandomIndex(arr.length);
//按照随机数组中的数字顺序,将所有的provider channel放入新的Channel数组中
for (int i = 0; i < result.length; i++) {
arr[i] = channelFutureWrappers.get(result[i]);
}
SERVICE_ROUTER_MAP.put(selector.getProviderServiceName(), arr);

}

@Override
public ChannelFutureWrapper select(Selector selector) {
return CHANNEL_FUTURE_POLLING_REF.getChannelFutureWrap(selector.getProviderServiceName());
}

/**
* 将ChannelFuture 按照weight 分配并乱序
* @param url
*/
@Override
public void updateWeight(URL url) {
List<ChannelFutureWrapper> channelFutureWrappers = CONNECT_MAP.get(url.getServiceName());

//根据权重值,创建对应数组权重越大index在数组中占比达
Integer[] weightArr = createWeightArr(channelFutureWrappers);
Integer[] randomArr = createRandomArr(weightArr);
ChannelFutureWrapper[] finalChannelFutureWrappers = new ChannelFutureWrapper[randomArr.length];

for(int i = 0; i < randomArr.length; i++){
finalChannelFutureWrappers[i] = channelFutureWrappers.get(randomArr[i]);
}

SERVICE_ROUTER_MAP.put(url.getServiceName(), finalChannelFutureWrappers);

}

private Integer[] createWeightArr(List<ChannelFutureWrapper> channelFutureWrappers) {

List<Integer> weightArr = new ArrayList<>();
for(int k = 0; k < channelFutureWrappers.size(); k++){
double weight = channelFutureWrappers.get(k).getWeight();
int c = (int) weight / 10;
for(int i = 0; i < c;i++){
weightArr.add(k);
}

}
Integer[] arr = new Integer[weightArr.size()];
return weightArr.toArray(arr);

}

/**
* 创建新的乱序数组
*/
public static Integer[] createRandomArr(Integer[] arr){
int total = arr.length;
Random ra = new Random();
for(int i = 0; i < total; i++){
int j = ra.nextInt(total);
if(i == j){
continue;
}

int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}

private int[] createRandomIndex(int len){
HashSet<Integer> set = new HashSet<>();
int[] arr = new int[len];
Random ra = new Random();

int index = 0;
while(index < len){
int num = ra.nextInt(len);
if(!set.contains(num)){
set.add(num);
arr[index++] = num;
}

}

return arr;

}
}

ConnectionHandler 封装

根据单一原则,我们把Connection所有的操作封装到 ConnectionHandler中。

BootStrap 便是 Netty的Bootsrap. 当程序启动时初始化时,我们就会循环遍历所有的服务,从Nacos中获取一个服务的所有实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void connect(String serviceName, String ip, int port) throws InterruptedException {
ChannelFuture channelFuture = bootstrap.connect(ip, port).sync();
ChannelFutureWrapper channelFutureWrapper = new ChannelFutureWrapper();
channelFutureWrapper.setChannelFuture(channelFuture);
channelFutureWrapper.setHost(ip);
channelFutureWrapper.setPort(port);
StringBuilder st = new StringBuilder();
st.append(ip).append(":").append(port);


//获取权重
Double weight = URL_MAP.get(serviceName).get(st.toString());
channelFutureWrapper.setWeight(weight);
SERVER_ADDRESS.add(st.toString());

List<ChannelFutureWrapper> channelFutureWrappers = CONNECT_MAP.get(serviceName);
if(CollectionUtils.isEmpty(channelFutureWrappers)){
channelFutureWrappers = new ArrayList<>();
}

channelFutureWrappers.add(channelFutureWrapper);
CONNECT_MAP.put(serviceName, channelFutureWrappers);

Selector selector = new Selector(serviceName);
VEKT_ROUTER.refreshRouterArr(selector);
}

通过 connect 方法连接,然后再将连接封装在 ChannelFutureWrapper 中,添加到 CONNECT_MAP中,然后刷新ROUTER。

那我们应该什么时候做路由操作呢?结合前文再获取ChannelFutureWrapper 即获取连接时是作路由的好的时间节点:

1
2
3
4
5
6
7
8
9
10
11
12
public static ChannelFuture getChannelFuture(String providerServiceName){

Selector selector = new Selector(providerServiceName);
ChannelFutureWrapper channelFutureWrapper = VEKT_ROUTER.select(selector);
if(channelFutureWrapper == null){
String message = String.format("no service %s provider", providerServiceName);
throw new RuntimeException(message);
}

return channelFutureWrapper.getChannelFuture();
}

因此我们将路由和连接组合起来了。

监听器模式与观察者模式介绍

观察者模式

这两者模式都是比较常用的设计模式。
我们先将一些两种设计模式的异同。

这个就是观察者模式的大概示意图,Subject可以对应多个观察者,每当subjectChanged() 发生改变时,通过Update() 方法将变更发送到每个Observer中。

因此如果我们要实现这个模式,需要实现Subject和Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Subject {

/**
* 注册定义
*/
void registerObserver(Observer observer);

/**
* 发送通知
*/
void notifyObservers(Object msg);

}

我们来实现一个具体的 Subject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConcreteSubject implements Subject {

/**
* 观察者集合
*/
private List<Observer> observers = new ArrayList<>();

@Override
public void registerObserver(Observer observer) {
// 添加订阅关系
observers.add(observer);
}

@Override
public void notifyObservers(Object msg) {
// 通知订阅者
for (Observer observer : observers) {
observer.update(msg);
}
}
}

Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Observer {
// 处理业务逻辑
void update(Object msg);
}


public class ConcreteObserver implements Observer {

@Override
public void update(Object msg) {
// 业务逻辑实现
System.out.println("ConcreteObserver 接收到主题的消息: " + msg);
}
}

这样就实现了一个观察者模式。

监听器模式

监听器模式主要组成部分主要有:

  • 事件源(事件发生的源头,比如按钮点击时间,按钮就是事件源)。
  • 事件对象:包装时间或者执行某个方法, 如点击事件,
  • 监听器: 监听器模式核心,定义事件发生后,将事件对象作为监听器的函数入参。

具体实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 事件接口
interface ClickEvent {
void onClick();
}

// 监听器接口
interface ClickListener {
void onButtonClicked(ClickEvent event);
}

// 具体事件源类
class Button {
private ClickListener listener;

public void setClickListener(ClickListener listener) {
this.listener = listener;
}

public void click() {
if (listener != null) {
ClickEvent event = new ClickEventImpl();
listener.onButtonClicked(event);
}
}

private class ClickEventImpl implements ClickEvent {
@Override
public void onClick() {
System.out.println("Button has been clicked!");
}
}
}

// 具体监听器类
class ButtonClickListener implements ClickListener {
@Override
public void onButtonClicked(ClickEvent event) {
event.onClick();
System.out.println("ButtonClickListener is notified!");
}
}

// 测试类
public class ListenerPatternExample {
public static void main(String[] args) {
Button button = new Button();
ClickListener listener = new ButtonClickListener();
button.setClickListener(listener);

button.click();
}
}

监听器模式的使用

可以看到这跟我们讲的简易监听器模式十分相似,不过我们有了一个VektRpcListenerLoader。负责将各种Listener注册到内部。

在Client启动初始化时,会将每个监听器注册到Loader中。

主要挑一下节点变化的监听器讲一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public void callBack(Object t) {
URLChangeWrapper urlChangeWrapper = (URLChangeWrapper) t;
List<ChannelFutureWrapper> channelFutureWrappers = CommonClientCache.CONNECT_MAP.get(urlChangeWrapper.getServiceName());
if(channelFutureWrappers.isEmpty()){
logger.error("[ServiceUpdateListener] channelFutureWrapper is empty");
return;
}

//最新的 provider
List<String> matchProviderUrl = urlChangeWrapper.getProviderUrl();
Set<String> finalUrl = new HashSet<>();
List<ChannelFutureWrapper> finalChannelFutureWrappers = new ArrayList<>();

// 移除旧URL
for(ChannelFutureWrapper channelFutureWrapper : channelFutureWrappers){
StringBuilder st = new StringBuilder(channelFutureWrapper.getHost()).append(":").append(channelFutureWrapper.getPort());
String oldServiceAddress = st.toString();

if(matchProviderUrl.contains(oldServiceAddress)){
finalChannelFutureWrappers.add(channelFutureWrapper);
finalUrl.add(oldServiceAddress);
}
}

//增加新的provider
//此时老的已经被移除,开始检查是否有新的url
//ChannelFutureWrapper是一个自定义的包装类,将netty建立好的ChannelFuture做了一些封装

List<ChannelFutureWrapper> newChannelFutureWrapper = new ArrayList<>();
for(String newProviderUrl : matchProviderUrl){
if(!finalUrl.contains(newProviderUrl)){
//新的 URL
ChannelFutureWrapper channelFutureWrapper = new ChannelFutureWrapper();
String host = newProviderUrl.split(":")[0];
Integer port = Integer.valueOf(newProviderUrl.split(":")[1]);
channelFutureWrapper.setPort(port);
channelFutureWrapper.setHost(host);
ChannelFuture channelFuture = null;

try {
channelFuture = ConnectionHandler.createChannelFuture(host, port);
channelFutureWrapper.setChannelFuture(channelFuture);
newChannelFutureWrapper.add(channelFutureWrapper);
finalUrl.add(newProviderUrl);

} catch (InterruptedException e) {
logger.error("ServiceUpdateListener callback error",e);
}
}

finalChannelFutureWrappers.addAll(newChannelFutureWrapper);
//最终更新服务
CommonClientCache.CONNECT_MAP.put(urlChangeWrapper.getServiceName(), finalChannelFutureWrappers);
}

}

上面代码注释里就可以看到,就是节点变化后,将节点从列表中取出修改后再放回到列表中。

监听器模式与观察者模式介绍

监听器模式与观察者模式介绍

观察者模式

这两者模式都是比较常用的设计模式。
我们先将一些两种设计模式的异同。

这个就是观察者模式的大概示意图,Subject可以对应多个观察者,每当subjectChanged() 发生改变时,通过Update() 方法将变更发送到每个Observer中。

因此如果我们要实现这个模式,需要实现Subject和Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Subject {

/**
* 注册定义
*/
void registerObserver(Observer observer);

/**
* 发送通知
*/
void notifyObservers(Object msg);

}

我们来实现一个具体的 Subject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConcreteSubject implements Subject {

/**
* 观察者集合
*/
private List<Observer> observers = new ArrayList<>();

@Override
public void registerObserver(Observer observer) {
// 添加订阅关系
observers.add(observer);
}

@Override
public void notifyObservers(Object msg) {
// 通知订阅者
for (Observer observer : observers) {
observer.update(msg);
}
}
}

Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface Observer {
// 处理业务逻辑
void update(Object msg);
}


public class ConcreteObserver implements Observer {

@Override
public void update(Object msg) {
// 业务逻辑实现
System.out.println("ConcreteObserver 接收到主题的消息: " + msg);
}
}

这样就实现了一个观察者模式。

监听器模式

监听器模式主要组成部分主要有:

  • 事件源(事件发生的源头,比如按钮点击时间,按钮就是事件源)。
  • 事件对象:包装时间或者执行某个方法, 如点击事件,
  • 监听器: 监听器模式核心,定义事件发生后,将事件对象作为监听器的函数入参。

具体实现方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 事件接口
interface ClickEvent {
void onClick();
}

// 监听器接口
interface ClickListener {
void onButtonClicked(ClickEvent event);
}

// 具体事件源类
class Button {
private ClickListener listener;

public void setClickListener(ClickListener listener) {
this.listener = listener;
}

public void click() {
if (listener != null) {
ClickEvent event = new ClickEventImpl();
listener.onButtonClicked(event);
}
}

private class ClickEventImpl implements ClickEvent {
@Override
public void onClick() {
System.out.println("Button has been clicked!");
}
}
}

// 具体监听器类
class ButtonClickListener implements ClickListener {
@Override
public void onButtonClicked(ClickEvent event) {
event.onClick();
System.out.println("ButtonClickListener is notified!");
}
}

// 测试类
public class ListenerPatternExample {
public static void main(String[] args) {
Button button = new Button();
ClickListener listener = new ButtonClickListener();
button.setClickListener(listener);

button.click();
}
}

VektRPC开发日志一:协议定义与调用流程

Version 1

Version 1

v1 主要是大致Rpc协议定义和远程调用流程的编写。

RPCProtocol

RPCProtocol 主要是三部分组成:

1
2
3
4
5
public class RpcProtocol{
private short magicNumber;
private int contentLength;
private byte[] content;
}
  • magicNumber:魔法值可以作为协议的标识符。作为 RPCDecoder 对于协议的鉴别
  • contentLength:content长度,当服务端接受能力有限的时候,可以对contentLength赋值。若哟读取到的网络数据包中contentLength大于预期就不读取。
    • 什么是接受能力有限的时候?
    • 应该设置多少的预期?
  • cotent: byte数组 作为数据核心内容

RPCInvocation

远程调用类

1
2
3
4
5
6
7
public class RpcInvocation{
private String targetMethod;
private String targetServiceName;
private Object[] args;
private String uuid;
private Object response;
}

相关字段分别对应的就是远程目标方法,目标的Service名称,以及这个方法需要的参数。

  • uuid:作用是将此次调用与请求线程对应以及对于reponse的设置
  • response: 远程调用的结果,调用结束后通过uuid设置给RpcInvocation。

在调用时,会将RpcInvocation这个类对象的实例序列化,然后放在RPCProtocol中,传递给Server。

RPCDecoder

这是通过继承 Netty 的ByteToMessageDecoder来实现对于RPCProcotol协议的解码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() >= BASE_LENGTH){
if (in.readableBytes() > 1000){
//防止收到一些体积过大的数据包
in.skipBytes(in.readableBytes());
}
int beginReader;
while (true){
//第一次为0
beginReader = in.readerIndex();
//标记readerIndex,可以通过resetReaderIndex,重新将buffer的readerIndex重置为标记的readerIndex
in.markReaderIndex();
//这里对应了RpcProtocol的魔数
if (in.readShort() == MAGIC_NUMBER){
break;
}else {
//如果不是魔数开头,说明是非法的客户端发来的数据包
ctx.close();
return;
}
}

//这里对应RpcProtocol的contentLength字段
int contentLength = in.readInt();
//说明剩余的数据包不是完整的,这里需要重置下readerIndex
if (in.readableBytes() < contentLength){
in.readerIndex(beginReader);
return;
}

//这里其实就是实际的RpcProtocol对象的content字段
byte[] data = new byte[contentLength];
in.readBytes(data);
RpcProtocol rpcProtocol = new RpcProtocol(data);
out.add(rpcProtocol);
}
}

目前遇到的一些问题是关于 TCP数据包黏包拆包的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

//原RPC——Decode代码
while (true){
//第一次为0
beginReader = in.readerIndex();
//标记readerIndex,可以通过resetReaderIndex,重新将buffer的readerIndex重置为标记的readerIndex
in.markReaderIndex();
//这里对应了RpcProtocol的魔数
if (in.readShort() == MAGIC_NUMBER){
break;
}else {
//如果不是魔数开头,说明是非法的客户端发来的数据包
ctx.close();
return;
}
}

//出事
while(true){
beginReader = in.readerIndex();
in.markReaderIndex();
if(in.readShort() == MAGIC_NUMBER){
break;
}
in.resetReaderIndex();
in.readByte();
if(in.readableBytes() < BASE_LENGTH){
return;
}
}

上面这一段代码,感觉更像针对于不存在黏包现象的,因为它直接判断第一个short是不是魔法值,儿没有考虑这种黏包情况:

请教了+哥,目前感觉综合了一个大致的合理的结束,粘的包是我们定义的数据包,及上面的连在一起的一整个长方形是一个TCP包。

此时TCP包会自动拆分首部信息之后,然后将Msg2(Part) 与 Msg1 读取到缓存区。此时Msg2变成了一个完整的数据包,此时缓存区里就有 Msg2、Msg1 两个完整数据包。

因此回到decode方法,我们去读取bytebuf中的一个数据包内容。然后依次处理数据包2 、1。

RPCEncoder

主要是将发出去的信息,encode没什么好说。继承MessageToByteEncoder<RPCProcotol>.

Proxy 动态代理

项目里还是用了动态代理。
主要可讲的就是 JDKClientInvocationHandler这个。
当调用代理的方法时, 就会触发其中的 invoke方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

RpcInvocation rpcInvocation = new RpcInvocation();
rpcInvocation.setArgs(args);
rpcInvocation.setTargetMethod(method.getName());
rpcInvocation.setTargetServiceName(clazz.getName());

//这里面注入了一个uuid,对每一次的请求都单独区分
rpcInvocation.setUuid(UUID.randomUUID().toString());
RESP_MAP.put(rpcInvocation.getUuid(),OBJECT);
SEND_QUEUE.add(rpcInvocation);
long beginTime = System.currentTimeMillis();
//客户端请求超时的判断依据
while (System.currentTimeMillis() - beginTime < 3 * 1000){
Object object = RESP_MAP.get(rpcInvocation.getUuid());
if (object instanceof RpcInvocation){
return ((RpcInvocation)object).getResponse();
}
}

throw new TimeoutException("client wait server's response timeout!");
}

使用方法时候,就触发,然后进行远程调用。

Other

cache包下有两个类,分别是ClientCacheServerCache

Client Cache 里面有两个东西:SEND_QUEUERESP_MAP.主要是依次发送请求的队列,以及一个以UUID为Key的ConcurrentHashMap来解决调用结束之后的结果填写。

ServerCache 里面存放的是PROVIDER_CLASS_MAP 是对应的是 RPCInvocation中的 targetServiceName

启动流程

目前主要是Client的启动流程。

前面说了,Proxy那儿,触发工作之后,将请求放到发送队列里。

因此在Client启动时,首先必然会创建BootStrap与 Server 连接。然后开启新的线程,异步取SEND_QUEUE 中的Invocation,再发送数据。

JVM内存管理策略

1. 运行时数据区域

Jvm 在执行Java程序时,会把它管理的内存划分为若干不同的区域。各有分工、

1.1 程序计数器

程序计数器(PCR) : 一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。

JVM概念模型中,字节码解释器会改变计数器选区下条需要执行的字节码指令。

JVM的多线程是由线程轮流切换、分配处理器执行时间实现的,一个处理器(一个内核)在同一时刻只执行一条指令。

切换后为了恢复到原来的位置,因此每个线程都有一个独立的PCR. 线程私有内存

计数器实际记录的是正在执行的虚拟机字节码指令的地址。若执行本地方法, 计数器为空。

本地方法: 用非Java语言(如C、C++等)实现的方法,也称为本地代码(Native Code)

1.2 Java 虚拟机栈

VM Stack:也是线程私有的。生命周期与线程相同。

虚拟机栈描述了Java方法执行的线程内存模型:

每个方法被执行时,Java 虚拟机会同步创建一个栈帧(Stack Frame)存储局部变量表、操作栈、动态连接、方法出口等。每方法执行完毕对应一个栈帧在VM Stack从入栈到出栈的过程。

  • 栈帧: 方法运行时期重要的数据结构

  • 局部变量表:存放编译器各种Java 基本数据类型、对象引用(Reference) .

  • 数据类型以局部变量槽(Slot)存储在局部变量表中

  • long 与 double 占据两个变量槽,其他的类型占用一个

  • 局部变量表的内存空间在编译期间就分配好了

  • 大小是指slot的数量的多少,具体 64bit 或 32bit由虚拟机实现

  • 注:基本类型的数组不再局部变量表,在Java堆中。

  • 两类异常:

    1. StackOverflowError: 线程请求栈深度大于虚拟机允许深度
    2. OutOfMemeoryError: 若JVM Stack 容量可动态扩展,而栈扩展无法申请到足够内存
  • HotSpot 虚拟机栈容量不可变,Classic 虚拟机可扩展

1.3 本地方法栈

本地方法栈与虚拟机栈作用相似。

区别:

  • VM Stack:为虚拟机执行Java方法服务

  • NativeMethod Stack:为虚拟机使用的本地方法服务。

有些VM 如 HotSpot将本地方法栈和VM栈合二为一。

1.4 Java Heap

Java Heap: 虚拟机管理的内存最大的一块,用来存放对象实例,“几乎”所有对象实例在这里分配内存。

在《Java 虚拟机规范》描述了:所有的的对象实例及数组都应该在堆上分配。

  • 但是随着JIT技术进步以及逃逸分析技术的日益强大,栈上分配、标量替换导致也不是这么绝对了。

  • 栈上分配:一种内存分配技术,它将对象的内存分配在线程栈上,而不是在堆上。这种技术可以减轻Java堆的负担,同时也可以提高程序的性能。栈上分配的前提条件是对象的生命周期非常短暂,即对象在方法内被创建并在方法结束时被销毁。由于这种技术可以避免频繁地访问堆,从而减少了内存访问的开销,因此可以提高程序的性能。

  • 标量替换:一种优化技术,它将一个对象拆分成多个基本类型的属性,并将这些属性分配在栈上或寄存器中,而不是在堆上。

  • 逃逸分析:一种静态分析技术,它可以分析程序中对象的生命周期,判断对象是否会被外部引用,即“逃逸”出当前方法或线程。如果对象不会逃逸,那么可以将对象分配在栈上或寄存器中。

Java 堆是被所有线程共享的一块内存区域,虚拟机启动时创建。也是垃圾收集器管理的内存区域,因此也叫GC堆

Tips:现代垃圾收集器大部分基于分代收集理论,会出现 新生代老年代永久代Eden空间等,仅仅是部分垃圾收集器的共同特性与设计风格。

从内存分配角度看,所有线程共享的Java堆中可以划分多个线程私有的分配缓冲区(Thread Local Allocation Buffer)来提升效率。

但是不会改变堆中存储的内容—对象实例

Java 堆逻辑上是连续而物理上不连续的。同时还可以被实现为固定大小、可扩展的。 通过 -Xmx-Xms设定。

异常: 内存中没有空间来进行对象实例的分配会抛出OutOfMemeoryError.

1.5 方法区

方法区:线程共享区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

《Java虚拟机规范》将其描述为堆一个了了逻辑部分。

JDK 8 以前,Java 程序员习惯在HotSpot上开发,很多人都更愿意把方法区称呼为“永久代”,但其实并不等价。

当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至方法区,或者说使用永久代来实现方法区而已

1.6 运行时常量池

运行时常量池: 方法区的一部分。

常量池表: 位于Class文件信息里,用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

2. HotSpot 虚拟机对象

Java 是一门面向对象的语言,时时刻刻都会有对象被创建。

2.1 对象创建

第一步

当 Java 虚拟机遇到一条字节码 new

  1. 首先检查指令参数是否在常量池中定位一个类的符号引用,检查引用类是否被加载、解析与初始化。否则执行类加载过程
  2. 类加载之后,虚拟机为新生对象分配内存,对象需要的内存大小,在类加载完成之后可以确定。
    • 如何分配?
      • **指针碰撞(BumpThePointer)**:堆中的内存都是绝对规整,所有被用过的内存放在一边,用一个指针用来作为分界指示点。分配多少空间,移动相同纪律。
      • **空闲列表(FreeList)**:堆中内存不规整,在分配的时候从列表中找一块足够的空间,然后更新列表。

JVM 分配方式由Java堆是否规整决定 -> 由采用的垃圾收集器是否有空间压缩整理(Compact) 能力决定。

  • Serial、ParNew等带压缩整理过程收集器使用指针碰撞。
  • CMS这种基于清除算法收集器,采用空闲列表的算法。

除了如何分配内存之外,如何在频繁创建对象下,保证指针的的移动的并发安全?

  1. 分配内存动作同步处理:虚拟机采用 CAS+失败重试 保证更新 的原子性。
  2. 按线程划分不同空间:每个线程在Java堆中预先分配一小块内存——本地线程分配缓冲。本地线程缓冲区使用完之后同步锁定。

内存分配完成之后,虚拟机会将分配的内存初始化为0。

第二步

JVM会对对象进行一些设置比如:对象HashCode(调用时生成)、实例属于哪一个类、如何才能找到类的元数据信息以及对象GC分代年龄等信息。

以上的信息都存储在对象头中,从之前的 JUC学习中我们知道,对于锁的状态(偏向锁、重向锁)等。

完成以上步骤,开始进入到构造函数。所有字段此时都默认为0,new 执行之后执行 <init>() 方法,按照程序编写的逻辑初始化。

2.2 对象内存布局

对象在堆内存中有三个部分: 对象头(Header)、实例数据(InstanceData)、对齐填充(Padding).

JVM.jpg

Java 中的对象头可以分为两类:普通对象与数组。

普通对象的对象头:

1
2
3
4
5
6
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组的对象头:

1
2
3
4
5
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|

两者相同的部分是:Mark Workd, Klass Word。

Klass Word 更多是与类相关的数据,类是静态的。而MarkWord则是对象运行时动态的数据,并且在32Bit与64Bit下的是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|


1
2
3
4
5
6
7
8
9
10
11
12
13
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|------------------------------------------------------------------------------|--------------------|

里面存放了 identity_hashcode: 就是对象hash值、age对象的年龄,biased_lock 与 lock共同组成了对于这个对象的锁的状态的描述。

下面这张图是上面的翻译版本。由于可用的空间比较细,但是需要表示的内容变多了。因此MarkWord会在复用空间。

image.png

具体锁相关的只是可以去看看JUC部分的知识。

  • 相关字段的解释:
    • lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。
    • biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
    • age4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
    • identity_hashcode25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
    • thread:持有偏向锁的线程ID
    • epoch:偏向时间戳。
    • ptr_to_lock_record:指向栈中锁记录的指针
    • ptr_to_heavyweight_monitor:指向管程Monitor的指针。

然后的实例数据部分,才是真正对于我们来说有效的数据,所有字段都记录了起来,存储顺序首到虚拟机分配策略和Java内部的设定有关。

分配顺序:longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs)

第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。HotSpot虚拟机自动内存管理系统要求对象的其实地址必须是8字节。

学过《计算机系统基础》的,是比较明白我说的意思的。

2.3 对象的访问定位

创建了对象之后如何使用?

Java 程序会通过栈上的 reference 数据来操作堆上的对象。

  • refernce: 一个指向对象的应用。

目前主流的访问方式:

  • 句柄方式:Java堆可能会划分一块内存作为句柄池,refernce中存储的是句柄地址,句柄中包含对象实例数据、类型数据各自的地址

    • 如图
      image.png
  • 直接指针方式:只需要访问一次,不需要间接访问一次句柄池,直接根据reference中的内存地址访问对象。但是放置对象更加困难。

image.png
快是真的快!因为Java中会经常访问到对象,多一次访问综合起来还是会有不小的开销的。

3. GC与内存分配策略

垃圾回收器(Garbage Collection): 用来对内存动态分配和回收的。

Java 程序会频繁创建对象,有些对象生命周期结束之后,GC为了复用空间会♻️回收对象。

因此我们应该如何判断对象已经死亡了呢?

3.1 对象的去世流程

首先死亡第一步要在医学上检验这个是不是真的去世了。

去世检验

目前主流的去世判断方法主要有下面两种:

  1. 引用计数法(JVM基本没用)
    • 在对象中添加引用计数器,有人引用就+1,失效-1. 当计数器为0时就可以回收了。
    • 优点: 原理简单、判定效率高(只需判断是否为0)
    • 缺点:
      1. 占用额外空间
      2. 其他情况待考虑

[!INFO]
引用计数法不能很好的解决,对象之间的相互依赖。

我们举个小例子:

1
2
3
4
5
6
7
class A{
private class B;
}

class B{
private class A;
}

如果存在 a、b对象,两者会相互依赖,引用计数器就无法为0。

  1. 可达性分析法
    是常用的方法
    把所有对象建成一个树的结构,根对象为GC Root. 根据之间引用的关系从上往下遍历,如果不能访问到的对象则可以被回收。

如下图所示:

GCRootSetPic

可作为根节点的条件:

  • 位于虚拟机栈的栈帧中的本地变量表中所引用到的对象(其实就是我们方法中的局部变量)同样也包括本地方法栈中JNI引用的对象。
  • 类的静态成员变量引用的对象。
  • 方法区中,常量池里面引用的对象,比如我们之前提到的String类型对象。
  • 被添加了锁的对象(比如synchronized关键字)
  • 虚拟机内部需要用到的对象。

示意图如下

一旦已经存在的根节点不满足存在的条件时,那么根节点与对象之间的连接将被断开。就是GC Roots和对象1之间断开。

也能较好的解决相互依赖的问题:

最终核验

死亡是一件大事,上面的检验完成之后。并不是立刻马上就入殡了。
JVM规定了至少检验两次之后,才能入殡防止诈尸。

第一次被标记之后,会进行一次筛选是否有必要执行 finalize() 方法。

  • 对象没有覆盖 finalize()
  • finalize() 已经被虚拟机调用
    上述两种情况都没必要执行。

如果有必要执行,JVM会将其放到一个 F-Queue ,在一个虚拟机建立的、低调度优先级的Finalizer线程执行**finalize()**。

对象可以覆盖finalize方法来抢救一下,但是只能抢救一次。因为finalize()不会执行第二遍。

以下行为不推荐
拯救对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class test{
public static test SAVE_HOOK = null;

public void isAlive() {
System.out.println("yes, i am still alive :)");
}

@Override
protected void finalize() throws Throwable {

super.finalize();
System.out.println("finalize method executed!");
test.SAVE_HOOK = this;

}

public static void main(String []args) throws Throwable {
SAVE_HOOK = new test();

//自救
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它 Thread.sleep(500);

if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}

执行结果:

1
2
finalize method executed! 
yes, i am still alive :)

有关方法区的回收

部分人认为方法区没有垃圾收集行为,实际上Java虚拟机规范也不要求虚拟机在方法区实现。

通常来说,对于方法区的回收其实收益是比较小的相较于堆中的回收。

[!INFO]
堆中的新生代回收率可以达到 70%-99%

所以方法区的回收有点吃力不讨好。

3.2 垃圾收集算法

各个平台虚拟机内存操作方法各异。

分代收集理论

  1. 弱分代假说: 绝大多数对象都是朝生夕灭的。
  2. 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡。

上面两个假说奠定多款常用GC的设计原则:将Java堆的划分出不同的区域,然后将回收对象熬过GC收集次数(Age) 分配到不同的区域。
比如这个区域都是朝生夕灭,可以较低代价回收大量空间。若是难以消亡的对象,那把它们集中放在一块, 虚拟机用较低的频率来回收这个区域,同时兼顾了垃圾收集的时间开销和内存的空间。

根据这个由此发展出了:标记——复制算法,标记——清楚算法,标记——整理算法等。

上面两种假说可以分别对应:新生代(Young Generation)、老年代(Old Generation)。但是后面的GC并没有遵循这一设计,因为对象之间可能跨代引用。

例如新生代对象被老年代引用,若要找出存活对象,不得不固定的GC Roots之外,还要遍历整个老年代中所有对象来保证准确,会增加负担。

因此增加了一个假说:

  1. 跨代引用假说: 跨代引用相对于同代引用占极少数

因此若存在跨代引用,应该是同时消亡或生存。

如果某个新生代对象存在跨代引用,由于老年代对象难以消亡,该引用会使得新生代对象在收集时同样得以存活,进而在年龄增长之后晋升到老年代中,这时跨代引用也随即被消除了。

因此不需要去扫整个老年代,只需在新生代上建立一个记忆集,将老年代划分为若干块,表示哪儿存在跨代,只有包含跨代引用的小块里会被加入GC Roots扫描。

  • Partial GC:不完整Java堆垃圾收集
    • 新生代收集(Minor GC/Youn GC): 新生代垃圾收集
    • 老年代收集(Major GC/Old Gc)
    • 混合收集(Mixed GC)
  • Full GC

具体收集算法

  1. 标记清除

这是最基础的垃圾收集算法。
首先表基础所有需要回收的对象,标记完成之后统一回收对象。或者相反,统一收回未标记对象。

但是这个缺点有点明显:
a. 效率不稳定,大量的对象需要回收的话,会频繁标记和清除
b. 内存空间碎片话,标记清除之后产生大量不连续内存碎片

  1. 标记-复制

Fenichel提出了半区复制 的垃圾收集算法。

将内存按容量分为大小相等的两块,只使用其中一块。若一块用完了,将还存活的对象复制到另外一块,然后将已使用的一次清楚。

如果都是存活的话,会有很大的内存复制开销,但是多数对象是可回收的。只需按顺序移动堆顶指针来分配内存。

目前大多数JVM优先使用此收集算法,IBM研究发现 新生代中98%对象熬不过第一次收集,因此没必要1:1划分新生代内存空间。

因此针对此现象,将新生代分为一块较大的Eden空间与两块较小Survivor空间。
每次分配内存时只使用Eden和其中一块Survivor.

HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1

  1. 标记—整理
    在老年代,对象存活率高,因此标记复制方法,会进行较多次复制操作。

若不想浪费50%空间,就要有额外空间担保。

image.png

标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的

经典垃圾收集器

image.png

上图中收集器处于不同分代,若俩之间有连线则可以搭配使用。

  1. Serial 收集器
    Serial收集器是最基础、历史最悠久的收集器。

这是一个单线程收集器,在收集时会暂停其他所有工作线程直到结束。

image.png

这其实体验很不好,就好比没过几分钟你电脑就卡死一下然后又正常。

虽然这很不好,并且HotSpot虚拟机开发团队一直在努力构思优秀的垃圾收集器,但是它仍然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。

因为它非常的简单和高效。

  1. ParNew 收集器

其实就是Seriral收集器的多线程版本,但是在收集时仍然要停止所有的用户线程。

image.png

在JDK7 之前的遗留系统是首选的新生代收集器。

在JDK5 时发布了CMS 收集器,这是第一款真正意义上并发的垃圾收集器,支持GC线程与用户线程同时工作。但是却无法与 JDK1.4中的Parallel Scavenge收集器配合。因此只能选择ParNewSerial

ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果。

  1. Parallel Scavenge/Old

一款基于标记-复制支持并行收集的新生代收集器。

PS收集器目标是达到一个可控制吞吐量。

$$吞吐量 = \frac{运行用户代码时间}{运行用户代码时间 + 运行垃圾收集时间}$$

而 CMS等收集器是注重尽可能缩短垃圾收集时用户线程停顿时间。

在JDK6时也推出了其老年代收集器Parallel Old,采用标记整理算法实现:

image-20230306165555265

与ParNew收集器不同的是,它会自动衡量一个吞吐量,并根据吞吐量来决定每次垃圾回收的时间,这种自适应机制,能够很好地权衡当前机器的性能,根据性能选择最优方案。

目前JDK8采用的就是这种 Parallel Scavenge + Parallel Old 的垃圾回收方案。
4. Serial Old收集器
这是Serial的老年代版本,同样也是标记-整理算法。

image.png

主要意义也是供客户端模式下的HotSpot虚拟机使用。
在服务端下也可以作为CMS失败后的后备收集器。

  1. CMS 收集器

CMS收集器一种以获取最短回收停顿时间为目标的收集器。

大量的Java应用集中在互联网网站服务端上,因此希望系统停顿尽量的短。

CMS收集过程有四个步骤:

  1. 初始标记(CMS initial mark)
    • 标记 GC Roots 可以直接关联到的对象,速度较快
  2. 并发标记(CMS concurrent mark)
    • 从GC Roots直接关联的对象遍历整个对象图
    • 耗时较长无需暂停用户线程
  3. 重新标记(CMS remark)
    • 为了修正并发标记期间,因为用户线程继续运作的导致的标记改动部分
    • 比初始标记时间长,比并发标记时间短
  4. 并发清除(CMS concurrent)
    • 清除标记的死亡对象,不需要移动存活的对象

运行示意图:
image.png

CMS的缺点:

  • 对处理器资源非常敏感,并发阶段虽然不会暂停用户线程,但是会占用部分线程导致应用变慢。
    • 默认启动的回收线程数:(处理器核心数量+3)/4
    • 当核心数 < 4时影响较大
  • 无法处理“浮动垃圾”,标记中用户线程仍然会产生新的垃圾对象, 只能等待下次标记。
  • 会产生大量的碎片空间,需要 -XX:+UseCMS-CompactAtFullCollection 来触发Full GC时合并碎片空间。
  1. Garbage First收集器

[!INFO]
垃圾收集器技术发展历史上的里程碑式的成果

JDK7 Update 4 是G1第一个商用版本,到了JDK8 Update 40 G1 提供了并发的类卸载支持。

成为了Oracle官方称为的 全功能垃圾收集器

HotSpot开发团队对于G1目标是替换CMS.

身为CMS接班人,Developers 希望那个有一款能建立 Pause Prediction Model的收集器。

  • 持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标

G1 面向的并不是新生代、老年代或者Java堆。G1 面向堆内任何部分组成回收集(Collection Set)进行回收。

因此它的回收标准不是xx代,而是哪儿的垃圾数量最多。

G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。

除此之外,Region中有一个Humongous特殊区域,来存储大对象(大小超过一个Region容量一半的对象)。

Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB。

G1收集器分区示意图

它的回收过程与CMS大体类似:

image-20230306165641872

分为以下四个步骤:

  • 初始标记(暂停用户线程):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
  • 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(暂停用户线程):对用户线程做一个短暂的暂停,用于处理并发标记阶段漏标的那部分对象。
  • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多个收集器线程并行完成的。

Other: 引用

JDK 1.2 之后Java将引用分为了:强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种。

  • 强引用: Object obj = new Object()
    • 这种就是强引用,只要引用关系还在就不会被回收。
  • 软引用:描述一些有用但非必须的对象
    • 只有即将发生内存溢出异常时才会列进二次回收。
  • 弱引用:描述非必须对象
    • 被弱引用关联的对象只能生存到下一次垃圾收集发生为止
  • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。

计算机网络基础

计算机网络

  • 概述
  • 物理层
  • 数据链路层
  • 网络层
  • 运输层
  • 应用层

    概述

  1. 因特网构成
  • 核心部分:路由器:为边缘部分提供服务的(提供了连通性和交换)
  • 边缘部分:主机(端系统):用户直接使用的(传送数据)
  1. 交换方式:
  • 电路交换
    • 通话时间内,两用户占用端到端通信资源,效率低下。
  • 报文交换
  • 分组交换 因特网
  1. 路由器主要任务
  • 实现分组交换
  • 储存转发
  1. 分组
    //todo 图1-11
    报文:整个要发送的数据块
    包:将报文分段加上首部,构成一个分组(包)。1500一个分段,有效数据1480

包头(首部)

目的地址,原地址等重要控制信息。一般20个字节

路由器只查错,不纠错。
分组可以走不同路径,有可能乱序。乱序交给上层应用处理顺序。

路径算法:rip(最短,必考)ospf(最快:不管路径)
互联网核心部分:高速链路链接

边缘部分:相对低速链路链接

  • 发送方:构造分组 发送分组
  • 路由器:缓存分组 转发分组
  • 接收方:接受分组 还原报文

分组交换优点:

  1. 高效:分组传输中动态分配传输带宽,通信链路逐段占用
  2. 灵活:为每个分组找最合适转发路由
  3. 迅速:分组为单位,可不先建立链接向其他主机发生分组
  4. 可靠:保证可靠的网络协议,分布式多路由分组交换网,有好的共存性。
    5.计算机网络定义和分类:
  • 按交换技术
  • 按使用者
  • 按传输介质
  • 按覆盖范围
  • 按拓扑结构
  1. 计算机网络性能指标
  • 速率
  • 带宽
  • 吞吐量
  • 时延(发送时延 传播时延 处理时延 排队时延)
  • 时延带宽积
  • 往返时间
  • 利用率:信道利用率 网络利用率
  • 丢包率:误码丢弃

sw:数据链路层
r:网络层
计算机网络最重要的功能:

  • 连通性
  • 共享

通信方式

  • 客户端-服务器(c/s)方式
    客户是服务请求方,服务器是服务提供方。
    客户和服务器都是指计算机进程。
    服务器软件特点:
  • 可同时处理对个远地或本地客户请求。
  • 一直不断运行,被动等待接受各个客户端通信请求。
  • 对等连接方式(p2p)
    两个主机在通信时不区分那个服务提供方,两个要有对等链接软件。

因特网核心部分

因特网核心部分是因特网最复杂的部分。

  • 网络核心部分起特殊作用的是路由器
    路由器:输入输出之间没有直接连线
  • 处理分组过程:
  • 将分组存入缓存
  • 查转发表,找某个地址应从那个地方转发
  • ?//todo
  1. 物理层
  • 介质
  • 物理接口
  • 如何表示0或1
  1. 数据链路层
    • 主机编址问题、如mac (同网段)
    • 如何从信号表示的一连串比特流中区分出地址和数据
    • 如何协调各个主机争用总线
  2. 网络层
    • 表示各网络以及网络中的各主机(跨网段 如ip地址)
    • 路由器如何转发分组,如何进行路由选择
  3. 运输层
    • 解决进程之间基于网络通信的问题
    • 出现传输错误时处理方法
      • 可靠谱传输
      • 流量控制
      • 拥塞控制
  4. 应用层
    • 通过应用进程间的交互来实现特定网络应用问题

计网原理体系结构

网络的性能指标

  1. ** 速率**
  • 比特(bit):是计算机中数据量的单位,也是信息论中使用的信息量的单位
  • 速率:即数据率(data rate)或比特率(bit rate): 是计算机网络中最重要的一个性能指标。速率的单位是 b/s,或kb/s, Mb/s, Gb/s 等
    • 速率往往是额定速率或标称速率。

    • pic20221128130944

  1. ** 带宽**
  • 带宽 :是数字信道所能传送的“最高数据率”的同义语,b/s.
    • 在模拟信号中各种不同那个批评率成分所占的频率范围
  1. 吞吐量
  • 吞吐量(throughput) :表示在单位时间内通过某个网络(或信道、接口)的数据量
  • 吞吐量受网络的带宽或网络的额定速率的限制
  1. 时延 : delay或latency
  • 发送时延 : 发送数据时候,数据帧从结点进入传输媒体所需时间按。

    • 发送数据帧的第一个比特算起,到该帧的最后一个比特发送完毕所需的时间

    • 计算方式: 数据帧长度(b) / 发送速率(b/s)

  • 传播时延 : 电磁波在信道中需要传播一定的距离而花费的时间

    • 信号发送速率和信号在信道上的传播速率不同的
    • 计算方法 : 信道长度(m) / 信号在信道上的传播速率(m/s)
  • 处理时延 : 交换结点为存储转发而进行一些必要的处理所花费的时间

  • 排队时延 : 结点缓存队列中分组排队所经历的时延

    • 排队时延的长短往往取决于网络中当时的通信量
  • 总时延 = 发送时延+传播时延+处理时延+排队时延

时延示意图
时延示意图

  1. 时延带宽积 = 传播时延 * 带宽

  2. 往返时间RTT:

  3. 利用率

    • 信道利用率指出某信道有百分之几的时间是被利用的(有数据通过)。完全空闲的信道的利用率是零
    • 网络利用率则是全网络的信道利用率的加权平均值
    • 信道利用率并非越高越好
  4. 丢包率

    • 在一定时间内,传输过程中丢失的分组数量和总分组数量的利率
    • 接口丢包率 节点丢包率 路径丢包率 网络丢包率等
    • pic20221128133200

常见网络体系机构

常见网络体系

第二掌 物理层

基本概念

物理层协议任务:

  • 机械特性 : 指明接口所用接线器形状尺寸、引脚数目和排列等
  • 电气信息 : 接口电缆各线电压范围
  • 功能特性 : 某条线上的某个电平电压意义
  • 过程特性 : 对于不同功能的各种可能事件的出现顺序

物理层:怎么才能在连接计算机的媒体上传输比特流

编码与调制

编码:

  • 不归零编码:

    • 存在同步问题
    • 高电平:1
    • 低电平:0
  • 归零编码:每个码元传输结束后都会归零

    • 接受方只需要在信号归零后采样。
    • 相当于把时钟信号编码在了数据里(自同步信号)
    • 编码效率低
      pic20221123103503
  • 曼彻斯特编码: 传统以太网 10Mb/s

    • 码元中间时刻跳变表示时钟又表示数据
    • 往下跳变 :1
    • 往上跳变 :0
    • 曼彻斯特编码示意图

10BaseT使用的是曼彻斯特编码

差分曼彻斯特:

调制方法:
基本调制方法

第三章 数据链路层

基本概念:

信道类型:

  • 点到点信道:PPP协议
  • 广播信道:CSMA/CD协议

媒体介入控制的基本概念:
共享信道要考虑的一个问题是如何协调多个发送和接受站点对一个共享传输媒体的占用。即媒体接入控制MAC

  • 频分多址: 用一个频带划分为多个频宽,一个频宽给不同用户

CSMA/CD (载波监听多址接入/碰撞检测)

  • 什么是碰撞:
  • 多址接入 MA
    • 多个站连接在一条总线上, 竞争使用总线
  • 载波监听 CS
    • 每个站发送帧之前检测是否有其他站点在发送帧。
    • 若检测到总线空闲96比特时间按, 则可可以发送
    • 若检测到总线忙, 则继续检测并等待总线转为空闲96比特时间,然后发送帧
  • 碰撞检测 CD
    • 每个正在发送的帧边发送边检测碰撞(” 边说边听”)
    • 一旦发现总线上出现碰撞, 立即停止发送, 退避一段随机时间后在发送
  • 先听先发送、 边听边发 、冲突停止、退避重发
  • 以太网还采用了一种强化碰撞措施: 当发送帧站点检测到碰撞, 除了立即体内址发送帧,还要继续发送32比特或者48比特认为干扰信号, 以便于足够多碰撞信号使所有站点都能检测到碰撞。

争用区:


最小帧长

64B<= 帧长 <= 512B
最大帧

截退二进制指数退避算法:

帧发送流程:

帧接受流程:

三个基本问题

  1. 封装成帧
    - 数据链路层给上层(网络层)交付的协议数据单元添加帧头帧尾使之成为帧。
    - 帧头帧尾包含控制信息
    -
    - 帧头帧尾的作用之一就是帧界定
    - MAC帧不需要帧定界符, 物理层在传输MAC层时会添加前导码:
    -

    - 若传输的数据中也有帧定界符号,就不能正确接受。因此需要在数据中相同的部分前插入一个转义字符。接受后提出转移字符。
    - 若内容中出现了转义字符,也是在前面添加转义字符。
    - 数据中封装成帧后, 每五个连续的1后面添加一个1
  2. 透明传输
    • 透明传输就是只数据链路层对于上层交付的阐述数据没有任何限制, 就像不存在数据链路层。
    • 面向字节的物理链路使用字节填充的方法实现透明传输
    • 面向比特…
  3. 差错控制

可靠传输

可靠传输不仅仅只出现在数据链路层, 其他各层都可以选择实现可靠传输。

以太网 (MAC地址, 集线器/交换机)

MAC IP 地址和ARP协议

Mac 地址: 以太网MAC子层所用地址
- 数据链路层
- 使用广播信道的数据链路层必须使用地址来区分主机
- 每个主机发送的帧中必须携带表示发送主机和接受主机的地址.
- MAC 地址一般固化在网卡的EEPROM中 (硬件地址), 有时也被称作物理地址,但是不是物理层
- 示意图
MAC地址一共六个字节 48未. EUI-48

MAC地址标识符: MAC地址标识符

单播MAC地址: 单播MAC地址举例

广播MAC地址:
-目的MAC : FF-FF-FF-FF-FF-FF (广播地址)
- 广播MAC举例:

Mac不具备区分不同网络的功能

IP地址 : 因特网上主机和路由器所用地址
- 网际层 TCP/IP体系结构网际层所用地址
- 构成: 网络号 + 主机号(标识同一网络不同主机 或者 路由器各接口)

从网络体系看IP地址和MAC地址 :

数据包转发过程中IP与Mac变化情况:
- 源IP地址 和 目的IP地址不便
- 源MAC地址 和 目的MAC地址逐个链路(或网络)改变
- 示意图

ARP协议: 属于TCP/IP体系结构网际层,
- 作用: 已知设备分配到的IP地址, 通过IP地址获取设备MAC地址

查询:
相应:

只能在同一网段下, 使用arp.
在统一网段下, 两者交互可以直接沟通不通过路由.

集线器 和 交换机

集线器HUB:

  • 集线器只工作在物理层
  • 集线器一般有少量纠错功能
  • 可以在物理层扩展以太网
  • 一般发广播

交换机:

  • 通常有多个段接口, 每个接口直接与一台主机和另一个以太网交换机相连,一般全双工模式
  • 以太网交换机具有并行性, 能同时连接多对接口,同时通行 . 无碰撞(不用csma/cd)
  • 交换机工作在数据链路层(包括物理层 ), 受到帧之后,在帧交换表查找目的MAC对应的接口号,然后 通过接口转发.

主要区别:

交换机自学习

  • 交换机工作在数据链路层.
  • 受到帧之后,在帧交换表查找目的MAC对应的接口号,然后 通过接口转发.
  • 交换机是一种即插即用设别, 随着网络中主机的通信,交换机通过自学习算法, 自动逐渐建立帧交换表.

直接受源地址学习

扩展以太网

第四章 网络层

负责在不同 网络之间尽力转发数据包, 基于数据包的ip目的地址转发。

  • 不负责 丢失重传 不负责顺序

概述

网络层主要任务 : 实现网络互联 进而实现数据包在各网络之间传输

主要问题
- 可靠传输还是不可靠传输
- 寻址问题
- 路由选择问题

因特网: 世界目前用户数量最多的互联网

  • TCP/IP协议栈

TCP/IP四层经典结构:

  • ICMP: 诊断网络连通性 Ping 命令
  • IGMP: 组管理协议–组播:视频分发
  • ARP: 广播协议

IPv4 地址概述

IPv4地址 : 给因特网上每一台主机(或路由器)的每一个接口分配一个全世界范围内的唯一32比特表示符号

IP地址 : 由因特网名字和数字分配机构ICANN分配

  • 我过向APNIC申请地址

IP地址编制三个阶段

IPv4地址 :采用点分十进制表示

分类编址IPv4 地址

A类:

B类:

C:

总结:

划分子网的IPv4地址

例如:B类地址,可以借用 16 位主机号中的8位为子网号
124.13.0.1 ,其中的 0 作为子网号码。

  • 32位子网掩码可以表明分类IP地址的主机号被借用了几个比特作为子网号
    • 子网掩码使用连续的比特1来对应网络好和子网号
    • 子网掩码用连续的比特0 来对应主机号码

如 228.75.230.0 分四个子网 228.75.230.0/26 , 子网掩码: 255.255.255.192

理解方法

做题方法:
确定可用地址范围 : +1 -2 法则

  • 第一段 0-64 : 228.75.230.1 - 228.75.230.62
  • 64 - 128 : 228.75.230.129 - 228.75.230.126
  • 128 - 192..
  • 192 - 255…

无分类编的IPv4

CIDR:

实际上将网络前缀相同的连续ip地址组成一个cidr地址块


前面20位是不变的。由128.14.35.7中的35确定了为0010。

聚合路由:

IPv4 地址应用规划

定长子网掩码

使用同个子网掩码划分子网.
每个子网IP数量相同,造成IP地址浪费

变长子网掩码 VLSM

使用不同子网掩码划分子网

每个子网分配的IP地址数量可以不同, 尽可能减少对于IP地址的浪费.

IP数据报的发送和转发过程

通过与运算:
A源地址
192.168.0.1
255.255.255.128

结果: 192.168.0.0

D目的地址
192.168.129
255.255.255.128
结果: 192.168.0.128

目的和源不处于同一个网路

中继器 和集线器工作在物理层,既不隔离冲突域也不隔离广播域
网桥和交换机 工作在数据链路层隔离冲突域但是不隔离广播域
路由器工作在网络层, 既隔离冲突域也隔离广播域

总结:

  • 主机发送数据报
    • 判断目的主机和自己是否在同一个网络
      • 若在同一网络,直接交付
      • 若不在同一网络, 属于间接交付
  • 路由器转发IP数据报:
    • 检查IP数据报首部是否出错
    • 根据IP数据报的目的地址在路由器中查找匹配条目

IP首部

IPv4 数据报首部格式

  • 首部长度
    • 4比特 表示首部长度 字段以4字节为单位
    • 最小十进制取值 : 5 只有20个固定部分
    • 最大十进制为 : 10 包含20字节和40 可变字节
  • 可选字段
    • 长度 : 1-40字节,用来支持排错 测量和安全措施
    • 一般不用
  • 区分服务:
    • 长度: 8比特 用来获取更好服务
    • 只有在使用区分服务时候,字段才起作用. 一般下不使用该字段.
  • 总长度
    • 长度: 16 比特 表示IP数据报的总长度( 首部+数据载荷)

数据报的分片:

  • 标识:

    • 16比特 属于同一个数据报的个分片数据报应该有同样的标识
    • IP软件维持一个计数器,每产生一个数据报,计数器值加1 并将此值赋给标识字段.
      • 相当于编了一个序号
  • 标志:

    • 3比特
      • DF位: 0 表示允许分片 1表示不允许分片
      • MF位: 1 表示后面还有分片 0 表示这是最后一个分片
      • 保留位: 必须为0
  • 片位移

必考题

分片举例子:

  • 生存时间 TTL:
    • 8比特

    • 最初以秒为单位最大255秒. 路由器转发ip数据报时, 将ip数据报首部中该字段的值减去ip数据报在本路由器上消耗的时间,若不为0 就转发.

    • 现在以”跳数”为单位, 路由器转发ip数据报时候.将IP数据报首部中的该字段值-1,若不为0就转发否则丢弃.

    • 协议:

      • 8比特 指明数据报部分是什么协议
  • 首部检验和
    • 16比特 检测首部在传输过程中是否出现差错 比CRC检验码简单
    • IP数据报每经过一个路由器, 都要重新计算首部检验和. 因为某些字段(生存时间 标志 片位移)可能比i安华
    • 网络层本身不提供可靠传输(数据链路层), 计算首部检验和耗时.IPv6 不计算首部检验和.

路由选择协议

  • 静态路由选择:
    • 人工配置的网络路由 默认路由等等
    • 优点 简单 开销小 缺点 不能及时适应网络状态的变化
  • 动态路由选择:
    • 通过选择协议自动获取路由信息

RIP协议

最先得到广泛使用的协议之一.

要求自治系统AS内每个路由器要维护从他自己到AS内其他每个网络记录. 称之为距离向量D-V;

RIP 使用跳数作为度量,来痕量到达目的网络的距离.

  • 直连网络距离定义为1
  • 非直连路由器的距离所经过路由器数量+1
  • 一条路径最多包含15个路由器. “距离等于16”时不可到达.

    跳数作为度量:

当到达同一目的地的网络有多条距离相等路由时候,可以进行等价负载均衡.

RIP三要点:

  • 只于相邻路由交换信息
  • 交换自己的路由表
  • 周期性交换

交换举例:

第五章 运输层

运输层解决的三个问题:

  1. 可靠传输
  2. 流量控制
  3. 拥塞控制

TCP的运输连接管理:

  1. TCP的连接建立: 三次握手
  2. TCP的连接释放: 四次挥手

运输层概述

物理层, 数据链路层, 网络层共同解决了将主机通过异构网络互联起来面临的问题. 实现主机到主机的通信.

  • 网络层不管你使用的是什么应用等等

计算机网络通信实体是位于通信两端主机中的进程.

运输层协议(端到端协议): 为运行在不同主机上的应用进程提供直接的通信
服务是运输层的任务.

示意图:

例如: AP3就是Web服务器 ,AP1 是一个浏览器. AP2 邮件客户端 AP4 邮件服务端.

运输层: 直接为应用进程间逻辑通信提供服务.

  • 向高层用户(引用层)屏蔽了下面的网络核心的细节, 他使应用进程看见的就好像是在两个运输层实体之间有一条端到端的逻辑通信信道.
  • 两种运输协议:
    • 面向连接TCP
    • 无连接UDP

运输层端口号 复用与分用的概念

  • 运行在计算机上的进程使用pid来表示.
  • 不同的操作系统使用不同格式进程标识符.
  • 使用统一的方法对TCP/IP体系应用进程进行标识
    TCP/IP体系运输层使用端口好区分应用层不同进程.
  • 16比特 0~65535
    • (服务器) 熟知端口号 0~1023 最重要的应用协议
    • (服务器) 登记端口号 1024~49151 没有熟知的端口号的应用程序使用 .必须在IANA按规定手续等级,防止重复. 如远程桌面是3389
    • (客户端) 短暂端口号 49152~65535 留给客户进程选择短暂使用.
  • 端口号只有本地意义. 为了标识本计算机应用层中各进程,不同计算机中相同端口号没有联系.

复用与分用

  • 熟知端口号

举个例子:

TCP和UDP对比:

重要问题

流量控制

让发送方发送速率不要太快要让接收方来得及接受.

利用滑动窗口机制可以很方便的在TCP连接上实现对发送方流量控制.

拥塞控制

拥塞: 对网络中某一资源需求超过了该资源能提供的可用部分, 网络性能变坏。

若出现拥塞不控制, 网络吞吐量随输入负荷增加而下降。

四种拥塞控制算法: 慢开始、 拥塞避免、快重传、快恢复

假定条件:

  1. 数据单方向传送,另一个方向只传送确认
  2. 接收方总是有足够大的缓存空间, 因而发送窗口大小由网络拥塞程度决定。
  3. 以最大保温段MSS个数为讨论单位,而不是以字节

慢开始和拥塞避免原理:

  • 发送方维护一个拥塞窗口(cwnd),取决于网络拥塞程度并且动态变换。

    • cwnd维护原则: 没有拥塞 窗口增大,出现拥塞窗口变小
    • 依据 : 没有暗示受到应当到达的报文(发生超时重传)
  • 发送发将cwnd作为发送窗口swnd = cwnd.

  • 维护一个慢开始门限ssthresh:

    • cwnd < ssthresh 时 使用慢开始算法
    • cwnd > ssthresh 时, 停止使用慢开始算法i改用拥塞避免算法
    • cwnd = ssthresh ,可以使用慢开始 也可使用拥塞避免算法

快重传和快恢复:

引入原因: 个别报文段会在网络中丢失,但是并未发生拥塞。

  • 导致发送方超时重传, 误认为网络发送拥塞
  • 发送方把cwnd设置为1 ,错误开启慢开始算法,降低效率

快重传: 可以让发送方尽早知道个别报文段发生丢失

  • 发送发尽快重传, 不等重传计时器
  • 接收方不要等待自己送数据是才进行捎带确认,应该立即确认
  • 受到失序报文段也要立刻发送对已收报文段重复确认。
  • 一旦发送方受到3个连续重复确认, 就要立刻重传

上图中的重复确认M2代表,M2前的包括M2的报文都收到了。重复确认M2就代表M3没受到。


可靠传输

TCP 基于以字节为单位的滑动窗口来实现可靠传输。

  • 可靠核心在于确认


TCP和UDP区别:

TCP 的连接管理

  • TCP是 面向连接的协议,击晕运输连接传输TCP报文段.

  • TCP运输连接的建立和释放是每一次通信中必不可少的过程.

  • 三个阶段

    1. 建立TCP连接
    2. 数据传输
    3. 释放TCP

1. TCP连接的建立

解决三个问题:

  1. 使得tcp双发能感知对方存在
  2. 使 tcp 双方能协商一些参数(如 最大窗口值, 是否使用窗口扩大选项和时间戳选项等等)
  3. 是 tcp 双方能够对运输实体资源 (如缓存大小 连接表中的项目等) 进行分配
  • TCP使用”三报文握手”建立连接:

SYN是表示连接创建的首部标识.
第一条握手是连接请求, 第二条连接请求确认.

  • 三报文握手最后一次握手是否多于?
    • 不多于, 第三题是普通确认SYN = 0.
    • 不多于举例子:

注意:

  • TCP 标准规定, SYN=1报文段不可携带数据, 但是要消耗掉一个序号.
  • 普通确认报文段如果不携带数据, 则不消耗序号.

2. 连接的释放

  • 四握手释放连接:

3. 首部格式

TCP采取了面向字节流方式.

TCP传输数据的时候, 从发送缓存中去除一部分或全部字节添加一个首部使其成为TCP报文段后发送.

  • TCP报文段首部格式(和IP数据报首部有点像):

  • 序号 seq: 占32比特, 取值范围[0,2^32-1] , 序号增加到最后一个后, 下个序号变为0.

    • 含义: 本TCP报文段数据在和的第一个字节的序号.
  • 确认号 ack: 32 比特, 取值范围[0,2^32-1] , 序号增加到最后一个后, 下个序号变为0.

    • 含义: 指出期望受到下一个TCP报文段数据载荷的第一个字节的序号, 同时也是对之前受到的数据的确认.
    • 确认标志位 ACK: 取值为1 确认号字段才有效
    • TCP规定, 连接建立后所有传送TCP报文段都必须把ACK置为1.
  • 数据偏移: 占4bit 并以4字节为单位

    • 指出TCP报文段的数据在和部分其实出距离TCP报文段起始处有多远.
    • 实际上表示了首部的长度
    • 固定20字节 为(0101), 最大60字节
  • 窗口: 16比特,以字节为单位, 指发送本报文段的一方的接受窗口

  • 检验和: 16比特, 检查范围包括TCP报文段首部和数据载荷两部分.

    • 在计算校验和时, TCP报文段前面加上12字节伪首部

UDP首部:

第六章 应用层

解决通过应用进程的交互来实现特定网络问题.-

概述

应用层是计算机网络体系的最顶层, 设计和建立计算机网络的最终目的.

C/S方式 和 对等(P2P方式)

DHCP

自动分配IP地址的

域名系统DNS

查询域名对应的IP地址.

  • Internet 采用层次树状结构的域名结构
  • 域名由若干分量组成, 各分量之间用”点”分开



层级是几个就需要几个域名服务器.

FTP

文件传送协议(FTP): Internet上使用最广泛的文件传送协议.
- 提供交互式访问: 允许客户指明文件的类型与格式, 摈弃而允许文件具有存取权限.
- FTP 屏蔽了各个计算机系统的细节,因而适合在异构网络中任意计算机之间传送文件.

  • 端口号:
    • 21 端口: 命令端口
    • 20端口: 传输数据
  • 控制连接在整个会话区间一直保持打开, 用于传送FTP相关控制命令.
  • 数据连接, 只有在需要传输数据的时候才会打开.

电子邮件


WWW

操作系统

操作系统

1. 操作系统

大型的程序系统,负责计算机全部软硬件资源分配等。

操作系统是指控制和管理整个计算机系统硬件和软件资源,并合理组织调度计算机的工作和资源的分配,以提供给用户和其他软件方便的接口和环境.

最基本的系统软件

1.1 操作系统目标

  • 有效性(改善资源利用率 提高吞吐率)

  • 方便性 (配置OS后计算机系统更容易使用)

  • 可扩充性 (OS应采取层次化结构)

  • 开放性 (OS应该遵循世界规范标准)

1.1.2 操作系统功能

  • 作为系统资源管理者

    • 处理机管理

    • 存储器管理

    • 文件管理

    • 设备管理

  • 作为用户和计算机硬件之间的接口

    • 命令接口:用户直接使用

      • 联机命令接口(交互式): 用户说一句 系统做一句

      • 脱机(批处理): 说一堆 做一堆

    • 程序接口(系统调用)(广义指令):允许用户通过程序间接使用,

    • gui

  • 作为最接近硬件的层次

    • 扩充机器

1.2 操作系统发展和分类

手工操作阶段

主要缺点: 用户独占全机,人机速度矛盾导致资源利用率极低.

单道批处理系统

映入脱机输出\输出技术(磁带), 监督程序负责控制作业输入输出

  • 优点: 缓解了一定程度的人机速度矛盾,资源利用率有所提升
  • 缺点: 内存中的只有一道程序运行,Cpu大量时间空闲等待I/O完成

多道批处理系统

引入中断技术.

优点: 多道程序并发执行,共享吉萨u年纪资源,资源利用率大幅提升,CPU和其他资源保持忙碌,系统吞吐量增大

主要缺点: 用户响应时间长,没有人机交互功能

分时操作系统

计算机以时间片为单位轮流为各个用户/作业服务. 主要有点: 用户请求可以被及时响应,解决人机交互问题,允许多个用户同时使用一台计算机,相互独立. 主要缺点: 不能优先处理一些紧急任务.

实时操作系统

主要优点: 可以优先响应紧急任务。

特点:即时性和可靠性

  • 硬实时系统:在严格时限内处理完事件

    • 导弹控制
  • 软实时: 偶尔违反时间规定

    • 12306 订票系统

1.3 操作系统基本特性与基本功能

并发 (基础特性)

  • 概念 :在内存中放多道作业,在一段时间看,每段作业都能不同程度向前推进,但是任意一个时间点只有一道占用CPU。

  • 微观上分片了,宏观上同时

  • 并行:并行是同时执行。微观上同时 宏观上同时

  • 串行:一个一个的执行。(单道)

  • OS并发性: 同时存在多个运行的程序

注意:程序实际上不能并发执行

  • 只有为程序创建线程才能并发执行

  • 进程是在系统中能独立运作并作为资源分配的基本单位。

  • 进程包括程序、数据、堆栈等。

  • 现代操作系统引入了最小独立运行的更小单位——线程 只有在

共享 (基础特性)

系统中资源供多个并发进程共同使用。

  1. 互斥共享方式

    • 一段时间只允许一个进程访问资源,临界资源独占资源。

    • 比图磁带打印机

  2. 同时共享方式

    • 一段时间允许多个进程访问

    • 比如磁盘

虚拟

通过某种技术将一个物理实体映射为若干逻辑上的对应物。

比如:CPU——多道程序设计技术(时分复用技术) 储存器——虚拟存储器技术(空分复用技术)

虚拟设备技术:SPOOLING

  • 时分复用技术

  • 空分复用技术

异步性

进程以不可预知运行速度向前推进。 只有系统有并发性,才有可能导致异步性.

1.3.1 操作系统的基本功能

处理机管理

处理机是计算机中最重要的资源。 现代操作系统郧西多个进程共享处理机。按照某种算法(分时、优先级)交替使用。 处理机管理:

  1. 进程控制

    1. 完成为作业创建一个或多个进程

    2. 撤销已结束进程

    3. 进程在运行过程中状态转换

  2. 进程同步

  3. 进程通信

  4. 调度

存储器管理:

  1. 内存分配

  2. 地址映射

  3. 存储保护

  4. 存储扩充

设备管理 文件系统管理 用户接口

存储程序计算机(冯诺依曼计算机模型):运算器、控制器、存储器、输入设备、输出设备

总线:。 内部总线:cpu芯片内部链接各元件 系统总线:链接cpu、存储器其和各种IO模块 通信总线:用于计算机之间通信。

CPU组成:运算器、控制器、 寄存器:通用寄存器、控制与状态寄存器、告诉缓冲寄存器

程序计数器PC作用:存放下一条指令的地址 pic20220909170604 指令暂存器IR:存放将要执行指令

程序状态寄存器PSW: 1.一类是体现当前指令执行结果的各种状态信息 2.

用户可见寄存器:所有程序

指令

一条高级语言翻译过来可能对应多条指令。

指令的格式: 指令的执行包含以下4个阶段:取指(instruction fetch)、指令解析(instruction decoding)、指令执行(instruction execution)。

  • 特权指令:是指在内核态下运行的指令,它对内存空间的访问基本不受限制

    • 不允许用户程序使用 非特权指令:是指在用户态下运行的指令。

处理器两种状态:

  • 用户状态(目态):CPU只执行非特权

  • 核心态(管态) : 特权非特权都可执行 用程序状态字寄存器某标志位标志。

程序分类

  • 内核程序: 运行在核心态

  • 应用程序: 用户态

用户态到内核态切换途径:

  1. 系统调用

  2. 中断

  3. 异常

系统调用图 大内核和微内核区别 优缺点 系统调用和库函数不同,系统调用使用的是系统提向程序提供的程序接口。用户只能通过程序访问这些接口。

中断

本质: 发生中断就意味着需要操作系统介入展开管理工作 中断发生后CPU立即进入核心态。 中断可以使CPU从用户态切换为核心态,是操作系统获得计算机控制权

中断是用户态到核心态的唯一途径。

  • 外中断(CPU外部,与指令无关)

    • 外设干预 :i/o操作完成发出的中断信号

    • 人工请求 : 用户强制中断程序

  • 内中断(信号来源CPU内部,与执行命令有关)

    • 自愿中断: 指令中断 (trap陷入指令)

    • 强迫中断

      • 硬件故障: 缺页

      • 软件中断: 整数/0

  • 内中断

    • 陷入 :系统调用

    • 故障 : 错误条件引起

    • 终止 : 不可恢复的错误

外中断处理过程:

  1. 执行每个命令之后,cpu检查当期是否有外部中断信号

  2. 检测到外部中断信号,保护被中断cpu环境

  3. 根据中断信号类型转入相应中断处理程序

  4. 恢复原进程CPU环境并退出中断,返回原进程继续执行

系统调用

用户想使用共享资源如打印机,只可以通过系统调用向操作系统发出请求,操作系统协调管理。

2. 进程

2.1 进程基本概念

前趋图:有方向的无循环图,记作DAG,用于描述程序/进程间之心前后关系。 pic20220914162551

p1为初始节点,p9为终止节点。同时还有程序\进程权重。

  1. 程序的顺序执行

    顺序性:在计算机系统中仅有一个程序运行,这个程序独占系统所有资源,不受外影响。执行完后另一个才开始。 前驱图

    封闭性:程序开始执行,结果不受外界因素。 可再现性:程

  2. 序结果和执行速度无关,给定相同输入一定有相同的结果。

  3. 程序并发执行 若干程序在系统中执行,宏观上一起执行,围观分时。执行时间重叠的。

    并发执行前驱图1

    无顺序性、程序无连续性、间断性、失去程序的封闭性

    不可再现性

进程的引入为了解决不可再现性。

进程实体: 程序(段)、相关数据段、进程控制块PCB

  1. 进程概念(有很多定义):进程实体的运行过程,是计算机进行资源分配和调度的一个独立单位。 程序与进程的对比 程序与进程

    进程包含程序,并且可以创建其他进程,而程序没有。 程序可以对应多个进程马,进程只能对应一个程序。 pic20220914171115

2.2 进程状态

三种基本状态:就绪、运行、阻塞(等待、睡眠、封锁)

  • 就绪状态:进程已经获得了除CPU的所有资源。

    • 就绪队列:所有处于就绪状态的进程。
  • 运行:正在运行的进程所处状态

    • 单处理机:一个进程

    • 多处理机:多个进程

    • 当前进程——运行状态的进程

  • 阻塞状态:若一个进程正等待某一事件发生,这是给ta CPU也无法运行

    • 阻塞队列:根据阻塞原因设置多个队列管理处于阻塞状态的进程

  • (创建态) 进程正在被创建,操作系统为进程分配资源,初始化pcb

  • (终止态) 正在被系统撤销,操作系统回收进程资源,撤销pcb

七状态模型:

2.3状态转换

进程状态转换

进程的状态信息保存在PCB中。

pic20220916163615

2.4进程控制块PCB

为了描述和管理进程运行每系统为每个进程定义了一个数据数据结构,称之为进程控制块(PCB)。

PCB记录了OS需要的用于描述进程当前情况以及控制运行全部信息 进程与PCB一一对应,PCB是os感知进程存在的唯一标识

  1. PCB作用

    1. 作为独立运行基本单位的标识

    2. 可以实现间断性运行方式(存放信息,可以阻塞 就绪)

    3. 提供进程管理所需的信息

    4. 提供进程调度需要的信息

      • 调度时要选择就绪状态 优先级等等等
    5. 实现与其他进程同步与通信

      • 信号量

    PCB应常驻内存

  2. PCB内容

    • 进程描述信息

      • 进程标识符(Process ID PID):唯一 通常整数

      • 进程名: 通常基于可执行文件名

      • 用户标识符(User ID UID): 进程组关系 比如SYSTEM re1ife root等等

    • 进程控制信息

      • 当前状态: 挂起 就绪 阻塞….

      • 优先级

      • 代码执行入口地址

      • 程序外存地址

      • 运行统计信息(执行时间、页面调度(分页系统))

      • 进程同步和通信

      • 阻塞原因

      • 进程队列指针

      • 进程消息队列指针

    • 所有拥有的资源和使用情况

      • 虚拟地址空间现状

      • 打卡文件列表

    • CPU现场保护信息

      • 寄存器值(RISC的通用、程序计数器PC、程序状态字PSW、地址包块栈指针)

      • 指向赋予该进程段/页表的指针 堆栈:储存进程执行的过程

  3. PCB表 系统把所有PCB组织放在内存固定区域, PCB表大小决定了系统中最东可同时存在的进程个数,称为系统并发度。

  4. 链接结构 相同状态进程PCB组成一个链表,不同状态对应多个不同 就绪链表 阻塞链表 pic20220916171649

  5. 索引结构

进程控制

进程控制是对系统内所有进程实施管理:

  • 创建一个新的进程

  • 终止一个已完成的进程

  • 终止一个因出现某事件无法运行的进程

  • 负载进程运行中状态转换

进程控制一般由OS内核原语实现。

OS内核:通常将OS一些与硬件精密相关的模块,注入中断处理程序、运行频率比较高的模块(时钟管理)、许多模块共用模块。安排在紧靠硬件的软件层次。 功能:

  • 支撑功能

  • 资源管理

微内核:机制与策略分离方法,只保留机制。

原语:一种特殊系统功能调用,完成一个特定功能,具有原子性,执行时不可以被中断

常用进程控制原语:

  • 创建原语

    1. 获取PID

    2. 申请空白PCB

    3. 为新进程分配资源 如内存

    4. 初始化进程控制快

    5. 将新进程插入就绪队列 fork() 创建一个继承的子进程(复制父进程的一切信息)

  • 终止原语

    1. 根据被终止进程pid,检索出pcb

    2. 若处于运行状态,应该立即终止并置调度标志为真

    3. 结束该进程所有子孙进程

    4. 将进程所拥有的资源交给父进程或系统进程

    5. 释放PCB

    6. exit() exit命令

  • 阻塞、唤醒原语

    • 阻塞是进程自身主动行为,当一个进程期待事件未出现时,调用阻塞原语将自己阻塞
    1. pic20220919083358
    • 唤醒:被阻塞进程所期待事情出现的时候,由相关进程调用唤醒。
    1. 移除阻塞队列

    2. 转变状态

    3. 插入就绪队列

    4. 重新调度

  • 挂起、激活原语

    • 挂起: 出现引起进程挂起事件是,系统利用挂起原语将指定进程或处于阻塞状态的进程挂起

    • 激活: 当发生激活进程的事件时,系统利用激活原语将指定进程激活

进程同步

  • 进程之间的两种制约关系

    • 间接相互制约关系\互斥关系

    • 直接相互制约关系\同步关系

      • 主要任务:是使并发执行的诸进程之间可以有效的共享资源和相互合作

      • 并发进程必须对邻接资源做出限制

  • 生产者-消费者问题 这是一种同步问题的抽象描述

    • 每个进程都可以使用和释放某类资源

    • 当某一进程使用某一资源可以看作消费,释放某一资源时就是生产.

    • 生产者消费者之间设置n个缓冲区缓冲池,生产者进程将他生产的产品放入缓冲区,消费者进程从缓冲区取走商品消费

    • 不允许消费者进程到一个空缓冲区取商品

    • 不允许生产者进程向一个已装满产品且尚未取走缓冲区存放

  • 邻接区:邻接段,每个程序中,访问临界资源那段程序

  • 同步机制规则:

    • 空闲让进

    • 忙则等待

    • 有限等待

    • 让权等待

TS指令 SWAP指令互斥

信号量机制

Dijkstra提出的一种卓有成效的的进程同步机制.

  1. 整形信号量

    • 把整形信号量定义为一个整数(任意整数) != int, 不能进行整数运算

    • 由两个标准原子操作wait(s) (P操作)与signal(S)(V操作)来访问 都是原语操作 P,V分别为荷兰语的test与increment

      wait(S):   while S <= 0 do no-op;
                S:=S-1;
      //wait相当于对资源的申请 资源> 0 才可以用

      signal(S): S:=S+1;
      //释放归还资源

      信号量手绘示意图

  2. 记录型信号量

    • 需要一个整数变量value 和一个连接所有阻塞进程的进程链表 queue

      struct semaphore{
        int value;
        pointer_PCB queue;
      }

    • P 操作

      P(S){
        s.value = s.value -1;
        if(s.value < 0){
          block //设为阻塞状态
          将该进程PCB插入阻塞队列末尾
        }
      }

      s.value负数代表阻塞进程个数

    • V 操作

      V(S){
        s.value = s.value+1;
        if(s.value <= 0){
          唤醒相应阻塞队列s.queue中阻塞的一个进程
          改变为就绪状态
          并将其插入就绪队列.
        }
      }

      先是s.value+1 ,若这个时候为0,就说明一开始<0.

      记录型信号量满足让权等待了

  3. AND型信号量 pic20220921170132 DE为临界资源,要互斥访问. 为D,E 分别设置互斥信号量 Dmutex , Emutex 初值为1

    会出现死锁问题.

    • AND同步机制基本思想: 将进程在整个运行过程中需要所有资源,一次性全部地分配给进程,待进程使用完之后在一起释放.只要尚有一个资源未分配给进程,其他所有可能为止分配的资源也不分配给他.

    • 实现wait操作中,增加一个”AND”条件,也称AND同步或同时wait操作(swait) 伪代码:pic20220921171851 Ssignal:pic20220921172136

  4. 信号量集

    • 一个需要n个某类临界资源是,就要进行n

    • pic20220921172621

  5. 信号量的应用

    1. 利用信号量实现进程互斥

      1. 职位临界资源设置一互斥信号量mutex,初始值为1

      2. 将个进程访问该临界资源的临界区CS置于wait(mutex)和signal(mutex)操作即可.

      3. 以上前面讲过

    2. 利用信号量实现前趋关系 pic20220921174457 若干进程为了完成共同任务并发执行,在进程中,有些要求有次序要求,有些没有,为了描述方便用图来描述.

      1. 方法一:

        1. 在需要顺序执行进程(P1 -> p2)之间设置一个公用信号量S(以此来表示后面进程是否可以开始运行).共两个进程共享,初始值为0

        2. 在进程P1 中,运行…,signal(S) 在进程P2 中,wait(S),运行….

      2. 方法二:

        1. 在需要顺序执行进程(P1 -> p2)之间设置一个同步信号量f(以此来表示前面进程是否完成).共两个进程共享,初始值为0

        2. 在进程P1 中,运行…,signal(f) 在进程P2 中,wait(S),运行….

经典进程同步问题

1. 生产者/消费者问题

pic20220923164657

通过一个有界缓冲区可以把一群生产者p1,p2,…pm,和一群消费者Q1,Q2,..,Qn联系起来 pic20220923164904

临界资源互斥访问,mutex = 1

  • 生产者:

    P;
    i = 0;
    while(1){
      CreateProduct(); //生产产品
      P(empty); //看看窗口里商品是不是满了
      P(mutex); //互斥访问
      put(buffer[i],object); //往buffer[i]放产品
      i = (i+1) % n;
      v(mutex);
      V(full); // 让full+1 ,代表增加了一个商品.
    }

  • 消费者:

    Q:
    j = 0;
    while(1){
      CreateProduct(); //生产产品
      P(full); //看看窗口有没有商品
      P(mutex); //互斥访问
      top(buffer[i],object); //往buffer[i]取产品
      j = (j+1) % n;
      v(mutex);
      V(empty); // 让empty+1 ,代表减少了一个商品.
    }

互斥信号量PV操作必须成对出现, empty的P和full的V必须在同一进程成对出现. 采用and信号解决: pic20220923171554

  • 如果缓冲区是无界的呢? pic20220923171946

2. 读者/写者问题

与消费者问题不同,读者进程在阅读后不会清除数据,不会改变数据。因此多个读者可以同时访问共享数据。

  • 写进程读进程同时共享数据会导致数据不一致。

  • 两个写进程同时共享数据可能导致数据错误覆盖。

要求:

  • 允许多个读者同时读

  • 只允许一个写者写文件

  • 任一写着在完成写之前,不允许其他读者或写着工作

  • 写者写操作之前应该让已有读者和写者全部推出。

互斥关系: 写写,写读,读读之间不互斥

读者写者问题解决方案

3. 哲学家就餐问题

pic20221127152226

思路:只存在互斥关系,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。 信号量设置: chopstick[5] = {1,1,1,1,1}, 并对哲学家0-4编号,哲学家i左边的筷子编号为i, 右边筷子编号为(i+1) % 5; 哲学家就餐解决方案

管程机制

  1. 引入原因: 可以减少大量同步操作分散在各个进程中.

  2. 为每个共享资源设立一个专门的管程,来统一管理各进程对于该资源的访问.

  3. 定义: 一个管程包含一个数据结构和能为并发进程所能执行(在该数据结构上)的一组操作.这组操作能同步进程和改变管程中的数据.

  4. pic20220923172552

  5. 三部分:

    1. 局部于管程的共享变量说明

    2. 对于该数据结构进行的一系列操作

    3. 对局部于管程的数据设置初始化的语句.

  6. 管程的语法 pic20220923172750

  7. 管程的特点

    • 局部数据变量只能被管程的过程访问,任何外部过程都不能访问

    • 一个进程通过调用管程的一个过程进入管程

    • 在任何时候,只有一个进程在过程中执行,调用管程的任何其他进程都能被阻塞,以等待管程变为可用的. pic20220923173209

  8. 条件变量

    • 管程内部同步机制

    • 每个条件变量表示一种等待原因,对应一个等待队列.

    • 可执行wait和signal两种操作,且置于wait和signal之前,如x.wait,x.signal

      • wait()

        • 将自己阻塞在等待队列中

        • 唤醒一个等待者或释放管程的互斥访问

      • signal()

        • 将等待队列中的一个线程唤醒

        • 若等待队列为空

  9. 利用管程解决生产者-消费者问题

    1. 建立管程pc

    2. put(item) 过程, 将生产产品投放到缓冲池中,并用整形变量count表示已有产品数目,count>=n时缓冲池已满

    3. get(item) 过程 从缓冲池中取走一个产品,当count<=0时,表示缓冲池空,消费者需等待 pic20220923175119

进程通信(IPC)

  • 内容:完成进程之间信息交换

  • PV 操作是进程间的低级通信,P,V为低级信息原语

  • 要在进程间传递大量信息可以用Send/Receive (高级通信原语)

类型:

  • 共享存储器系统

  • 消息传递系统

  • 管道通信(共享文件方式)

  • 客户机/服务器方式 : socket、RPC

共享存储器系统

  1. 共享数据结构通信方式 诸进程公用某些数据结构。 数据量小(低级通信)

  2. 基于共享存储区的通信方式

    • 高级通信,在存储器中划出一块共享存储区

    • 通信前向系统申请共享存储区的一个分区,指定该分区的关键字,若系统以已经给其他进程,则将描述符返回给申请者

    • 申请者吧获得的共享存储分区连接到本进程,此后可以读写该公用分区

    • linux共享存储区示意图

      • Linux 系统:

        • shmeget 创建共享段

        • shmat 将共享段映射到地址空间

        • shmdt 取消共享段到进程地址空间的映射

        • shmctl 共享段控制

    • 优点:

      • 最快的方法

      • 一个进程写另一个进程立即可见

      • 没有系统调用干预

      • 没有数据复制

消息传递系统

进程间数据交换以消息(有格式的)为单位,利用系统通信原语实现通信。 与信息不同,消息有格式。 操作系统隐藏了通信的实现细节,简化了通信程序编制的复杂性,因而得到了广泛的应用。 pic20220928165159

  • 直接通信:发送进程直接把消息发送给接受者,并将它挂在接收进程的消息缓冲队列上。接受进程从消息缓冲队列取得消息。

    • 也成为消息缓冲通信

    • 实现

    • 在操作系统空间设置一组空白缓冲区。

    • send:pic20220928174016

    • receive:pic20220928174025

    • 消息缓冲区结构:pic20220928174350

    • PCB中有关通信数据:pic20220928174638

    • 用P、V实现send原语:

      • pic20220928174706
    • 用P、V实现receive:

      • pic20220928174843
  • 间接通信:发送进程将消息发送到某种中间实体(信箱),接受进程从中取得消息。

    • 信箱通信(在网络中称为电子邮件系统)

    • 信箱结构: 信箱结构

    • 类型:

      • 私有信箱:用户进程自己创建的信箱。是进程的一部分,可用单向链路实现。

      • 公用信箱: 操作系统创建,系统核准进程都可以使用,采用双向通信链路

      • 共享信箱:某进程创建并指明可共享

    • 通信方式

      • 信箱的创建和撤销

      • 信箱中的消息的发送和接受

      • send实现:吧信件M送到指定信箱中:

        1. 查找指定信箱

        2. 若信箱未满,则把M送入信箱且唤醒“等信者

        3. 若信箱已满,置发送信件进程“等信箱”状态

    • receive :

      1. 查找指定信箱

      2. 若信箱有信,则去除一封信存于X中唤醒等信箱者

      3. 若信箱无信,置接受信件进程“等信件”状态

管道通信

是最初UNIX IPC形式,能够传送大量数据,被广泛采用。 管道: 适用于连接一个读进程和一个写进程的文件。pipe文件,可以双工也可以是单工

  • 写进程 以字符流的形式将大量数据送入管道,而读进程可从管道中接受数据。

  • 分类:

    • 无名管道:临时文件,同祖先进程间的通信 pipe()

    • 有名管道:长久文件,任意进程读写 mkfifo()

管道和消息队列的区别:

  1. 管道外存,消息队列内存

  2. 管道中的消息是无界的

系统调用:

  • 读管道 read(fd, buffer,nbytes)

    • scanf()实现
  • 写管道 write(fd, buffer,nbytes)

    • printf实现
  • 创建管道 pipe()

管道命令符: | pic20220928172159

进程同步方式

  • 发送进程阻塞、接受进程阻塞 -> 分布式系统

  • 发送进程不阻塞、接受进程阻塞 -> 客户机、服务器

  • 发送进程和接受进程均不阻塞 -> 直接通信方式

多线程技术

  1. 线程的引入

目的:减少程序并发执行所付出的时空开销

进程是资源分配和调度基本单位。在创建、撤销、切换中,系统必须为之付出较大时空开销。所以系统进程数量不易过多,切换频率不能过高,限制了并发程度的进一步提高。

  • 为了解决上面的问题,引入线程。线程是进程中的一个实体,是被系统独立调度和分派的基本单位。

  • 线程基本不拥有系统资源,只拥有少量必不可少的资源:程序计数器、一组寄存器、堆栈

  • 线程可以与同属一个进程的其他线程共享进程所拥有的全部资源

  • 一个线程可以创建和撤销另一个线程

  • 同一个进程多个线程之间可以并发执行。

  1. 线程与进程的对比

    • 每个进程都拥有多个线程,至少有一个

    • 线程有进程许多特征,故又称为轻型进程

    1. 调度

      • 传统和os里,拥有资源、独立调度和分派的基本单位是进程,引入线程的系统,线程是调度和分派的基本单位。进程是拥有资源的基本单位

    2. 并发性

      • 引入线程中,进程间可以并发,同一进程各线程之间也能并发执行。有更好的并发性
    3. 拥有资源

      • 进程都是拥有资源的独单位,但是线程可以访问统一线程的全部资源
    4. 独立性

      • 进程间相对独立

      • 进程内部线程之间独立性较差

      • 线程之间无保护

    5. 系统开销

      • 进程的创建和撤销远大于线程的创建和撤销,进程切换时当前CPU环境要保存,新进程CPU环境要设置等等。线程只需要保存和设置少量寄存器。

      • 同一进程内各个线程由于拥有相同的地址空间,他们之间的同步和通信实现也更容易

    6. 多处理系统的支持

      • 传统OS以进程为单位分配处理器,一个进程只有一个

      • 引入线程,以线程单位分配处理器,一个进程可以分配多个处理器,让多个线程并行执行 image-20220929143855883

  2. 用户级线程和内核支持线程

    1. 内核支持线程: 线程依赖于内核,无论进程中的线程还是系统进程的线程创建、撤销、切换都由内核实现。

      • 所有线程管理核心完成

      • 没有线程管理,核心提供API

      • 和兴维护进程和线程上下文

      • 线程之间的切换需要核心支持

      • 以线程为基础核心调度

      • 优缺点:

        image-20220929150725097

    2. 用户级线程:内核不知道用户线程的存在。 用户级线程

    • 由应用程序完成所有线程的管理,通过用户空间的线程库(一组管理线程的函数)提供一个运行管理系统(运行时系统)

    • 线程切换时不需要核心态的支持

    • 调度是应用特定的

    • 优点:

      • 线程切换不需要核心(快)

      • 调度使应用程序特定的:可选择最好的算法

      • ULT 可以运行在任何操作系统上,可以在不支持线的OS上实现

    • 缺点:

      • 大多数系统调用是阻塞的,因此核心阻塞进程,所有线程阻塞

      • 核心只分配给进程

    1. 混合实现

      image-20220929151006265

  3. 线程应用实例

    • 当一个应用由几个独立部分组成,这几个部分不需要顺序执行,每个部分可以以线程方式实现。

    • 一个线程因IO阻塞时候,可以切换到统一应用的另一个线程。

^618723
一个web server的例子

处理机调度与死锁

低级调度:进程调度,选择一个就绪进程占有处理机任务 中级调度: (内存调度)以进程微单位,在内外存换进换出,提高内存利用率. 高级调度: (作业调度)只在多道批处理器里存在

从系统角度调度算法更关注: 吞吐量、处理器利用率

周转时间: 从提交到最后系统完成的时间跨度。 对于用户来说很重要 平均带权周转时间: 作业的周转时间 / 真正需要系统的时间

  1. 作业的周转时间可用公式表示如下:

    • 周转时间 = 作业完成时间-作业提交时间
  2. 平均周转时间是指多个作业周转时间的平均值:

    • 平均周转时间=(作业 的周转时间+…+作业 的周转时间)/
  3. 带权周转时间是指作业周转时间与作业实际运行时间的比值:

    • 带权周转时间=作业周转时间/作业实际运行时间
  4. 平均带权周转时间是指多个作业带权周转时间的平均值:

    • 平均带权周转时间 =(作业 的带权周转时间+…+作业 的带权周转时间)/
  5. 等待时间 等待时间指进程处于等处理机状态的时间之和,等待时间越长,用户满意度越低。

  6. 响应时间。 响应时间指从用户提交请求到系统首次产生响应所用的时间,在交互式系统中,周转时间不可能是最好的评价准则,一般采用响应时间作为衡量调度算法的重要准则之一。

  7. 响应比:

响应比 = (等待时间+要求服务时间) / 要求服务时间

作业调度和算法

作业调度任务: 外存中选择作业调入内存 作业调度的主要决策:

  1. 接纳多少取决于系统多道程序度(根据计算机系统规模、运行速度、作业大小)

  2. 接纳哪些作业取决于系统调度算法

    1. 先来先服务调度算法 FCFS : 等待时间最长的。 pic20220930164847

      • 周转时间: 完成时间 - 到达时间

      • 带权周转: 周转时间 / 服务时间

      • 公平、简单、有利于长作业不利于短作业、有利于CPU密集型作业,不利于IO密集类作业

      • 几乎所有进程都交替出现计算和IO请求

    2. 短作业 SJF : 估计运行时间最短的 pic20220930165856 带权周期时间明显变短 pic20220930170503

    3. 优先级调度算法 PAS: 优先级高的 调入,

      优先级越大,优先级越高。

      • 非抢占式优先级调度: 每次调度选择当前已经到达且优先级最高的进程。当前进程主动放弃处理机是发生调度。

      • 抢占式:每次调度选择当前已经到达且优先级最高的进程。当前进程主动放弃处理机是发生调度。就绪队列改变时候也要检查是否需要调度。

      • pic20221127170013

    4. HRRN相应度高的 完成在后备队列选择一个或多个优先级最高的U在哦也,将他们调入内存,为他们分配资源、创建进程,放入就绪队列。

      • 相应比 =( 服务时间+ 等待时间) / 服务时间 = 1+ 等待/服务 调度方式
    5. 非剥夺调度方式,又称非抢占方式。非剥夺调度方式是指当一个进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件而进入阻塞态时,才把处理机分配给更为重要或紧迫的进程。

    6. 剥夺调度方式,又称抢占方式。采用剥夺式的调度,对提高系统吞吐率和响应效率都有明显的好处。但“剥夺”不是一种任意性行为,必须遵循一定的原则,主要有优先权、短进程优先和时间片原则等。

低级调度(进程调度)

进程调度任务:

  • 保存处理机现场信息

  • 按某种调度算法选进程

  • 把处理器分配给进程

    • 有分派程序把处理器分开配给该进程,设置选中进程的的处理机现场信息,交处理机控制权给进程运行
  • 进程调度机制: pic20220930173052

    • 降薪就绪进程按一定策略插入到相应就绪队列合适位置

    • 进程调度方式:

      • 非抢占式: 不允许某进程抢占已经分配出去处理机

      • 抢占方式:允许调度程序根据某种原则,暂停正在执行进程,将处理机重新分配给另一进程。(优先权原则、短进程优先、时间片原则)

    • 策略(排队器):

      • 最短剩余时间

        • 对SPF增加了抢占机制版本

        • 调度程序总是选择预期剩余时间最短的进程

        • 优缺点 在周转时间上,SRT比SRF性能好,必须

    • 优先级确定方法

      • 进程类型:系统进程高于用户进程

      • 对资源要求:要求少运行时间短的先走

      • IO越高越先调度

基于时间片的轮转调度算法(RR) 基本原理:

  • 把CPU划 分成若干时间片

  • 按顺序给FIFO就绪队列中的每一个进程

  • 时间片用完时(时钟中断请求),即使没有执行完毕,也会剥夺该进程的CPU,将进程排在就绪队列末尾,同时系统选择队首进程运行. 切换时机:

  • 时间片内进程已经完成,立即激活进程调度程序

  • 时间片用完后,计时器中断处理程序被激活,送当前进程到就绪队列 时间片大小的确定: pic20221012163744 q: 时间片大小 q小:对短进程有利,但是切换过于频繁. q大:每个进程在一个时间片内完成,退化为先来先服务 q的选择:要略大于交互式任务的时间,大于10倍进程切换时间,

多级队列调度算法(MLQ):

算法思想: MLQ算法调度思想 例如:80%CPU调度交互式任务 20%CPU后台任务

多级反馈队列调度算法(MLFQ): MLFQ调度算法思想

pic20221127170650

多级: 队列之间的级别不同,编号越大级别越低.

有可能一直有新进程进入就绪队列1 ,造成饿死现象.

反馈:就是进程的降级.

该算法是抢占式算法,若队列1中不存在任务,此时正在执行队列2 中的任务则,当有新的任务时立即终止并且将终止的任务移到当前队列的末尾,执行新任务。

基于公平原则的调度算法:主要考虑调度的公平性.

保证调度算法:

  • 性能保证,而非有限运行

  • 如保证处理机分配的公平性(处理机的时间为1/n) 公平分享调度算法:

  • 调度的公平性主要针对用户来说

  • 使所有用户能够获得相同处理时间或时间比例 调度算法对比: pic20221012171634 pic20221012171645

Linux系统调度基于调度器类: 不同调度器类有特定的优先级

  • 默认: Stop_Task > Real_Time > Fair > Idle_Task

  • pic20221012171846

死锁

死锁的基本概念

基本概念:一组进程中的每个进程都无限等待被该组进程中另一个进程所占有的资源,而处于僵持界面,若无外力作用无法向前推进的这种现象称为进程死锁(Deadlock),这组进程称为死锁进程.

  • 死锁: 各进程互相等待对方手中资源,导致进程阻塞无法向前推进.

  • 饥饿:长期得不到想要的资源,某进程无法向前推进的现象.

竞争资源引起进程死锁:

  • 可剥夺和非可剥夺资源

    • 可剥夺: 进程在获得这个资源后可再被其他进程或系统剥夺

    • 非可剥夺: 资源被系统分配给某个进程后就不能强行收回,只能自己释放

    竞争临时性资源(进程通信时1 pic20221012174459

死锁条件:

  • 互斥

  • 非可剥夺资源 : 进程获得资源未使用完之前,不可被其他进程强行夺走

  • 请求保持:进程已经保持了至少一个资源,但是又提出了新资源请求。被其他进程占有。

  • 环路条件(循环等待条件): 存在一种进程的循环链,链中的每一个进程已获得资源同时被链中的下一个进程所请求.

    • 发生死锁时一定有循环等待,但是发生循环等待未必死锁。

pic20221014162051

发生死锁的情形

  1. 对于系统资源的竞争

  2. 进程推进顺序非法,请求和释放资源顺序不当

  3. 信号量使用不当

  • 总结: 对于非剥夺资源的不合理分配,可能会导致死锁。

死锁处理策略

  1. 预防死锁

  2. 避免死锁

  3. 死锁的检测和解除

处理机调度与死锁

死锁的预防

设置不同资源分配算法.

  1. 破坏互斥条件

    • 对于某些硬件资源, 可以采取特殊技术允许同时访问.(SPOOLING)

      • 对于软件资源无法实现
  2. 破坏请求和保持条件

    • 静态分配法)在执行时不再提出资源分配 事先一次性申请所需全部资源,如果系统有足够资源则完全分配

    • 在等待时不保持任何资源 只要有一个请求不可用,其他可用资源都不分配给他

        • 优点 :实现简单 安全

        • 缺点: pic20221014163520

  3. 破坏不可剥夺条件

    • 请求不到要释放: 一个已有资源的进程, 若他再提出新资源要求而不能立即得到满足时, 他必须释放已有所有资源. 有需要再申请

      • 缺点: 实现复杂要付出很代价

      • 优点: 资源利用率相比于静态资源分配 资源利用率高

    • 当某个进程需要的资源被其他进程占用的时候,可以有操作系统将想要的资源强行剥夺。(要考虑优先级

      • 缺点:实现复杂,被释放的进程之前的工作失效, 降低系统吞吐量,导致饥饿
  4. 破坏环路条件 系统将所有资源按类型进行排序, 并赋予不同序号, 所有进程对资源请求必须严格按照资源序号递增的次序提出. pic20221014164428

pic20221014164931

pic20221014165133

死锁避免

安全序列:如果系统可按照某种顺序为某个 进程一次分配所需资源,直至所有进程完成

安全状态:只要能找到一个安全序列就是安全状态。
不安全状态 : 找不到一个安全序列。

  • 安全状态一定不会发生死锁,不安全状态不一定会发生死锁。
    pic20221014165726

举例子:
pic20221014170009

由安全状态向不安全状态转换: 如果不按照安全蓄力分配资源, 系统会由安全状态向不安全状态转换/ 如在T0后P3要求一台打印机,若真的分了,就会 进入不安全状态:

pic20221014170512

!!!不安全不等于死锁

利用银行家算法避免死锁: Dijkstra 提出

更快找到安全序列:

手算安全序列

假设系统之中有n个进程,m种资源

  • Max 矩阵: 最大需求矩阵
  • Allocation:已分配矩阵
  • Need : Max - Allocation
    pic20221014172351 pic20221014172600

银行家算法具体流程:
银行家算法具体流程

步骤:
1. 检查本次申请是否超过之前申请最大需求数目
2. 检查此时系统剩余可用资源是否满足本次需求
3. 试探分配,修改数据
4. 用安全性算法检查此次分配是否会导致系统进入不安全状态

安全性算法: 检查当前剩余可用资源是否能满足某个进程的最大需求,如果可以加入安全序列,并将此进程持有资源全部回收。不断重复上述,观察最终是否所有资源都可加入安全序列。

死锁的检测和解除

前置条件

  1. 用某种数据结构保存资源请求和分配信息
  2. 提供一种算法检测系统是否进入死锁
  • 数据结构示意图:
  • 检测算法:如果能消除所有边 => 能找到安全序列, 反之不能

解除死锁

  1. 资源剥夺法 : 挂起某些死锁进程,抢占他的资源,将这些资源分给其他死锁进程.但应该放置被挂起进程长时间得不到资源而饥饿.
  2. 终止进程法: 强制撤销部分,甚至全部死锁进程
  3. 进程回退法: 让一个或多个死锁进程回退到足以避免死锁的地步.

内存

内存基本概念

内存是存放数据的硬件.程序要先放到内存里才能被CPU处理.

  • 内存地址: 从0开始每个地址对应一个存储单元
  • 存储单元: 内存中一个一个小房间
  • 按字节编址: 每个储存单元大小为1字节, 1B
  • 按字编址: 每个储存单元大小取决于计算机的字长.
  • 示意图: 示意图

运行的原理:指令

运行流程:

指令中直接给出了变量的实际存放地址, 但是实际中并不能知道, 所以一般使用逻辑地址.(相对地址)

逻辑地址转换物理地址方法:

  1. 绝对装入 : 在编译时知道内存的绝对地址 (单道程序环境)
  2. 静态重定位: 装入时对地址重定位
  3. 动态重定位: 一直是逻辑地址, 运行时才进行地址转换,需要重定位寄存器.

内存管理

  • OS提供功能:

    1. 操作系统负责内存空间的分配和回收
    2. OS需要提供某种技术上从逻辑上对内存空间的扩充
    3. OS要提供地址转换功能,负责逻辑地址和物理地址转换.
    4. OS提供内存保护功能, 保证各个进程在各自储存空间内运行互相不干扰. 检查是否越界
      1. 上下限寄存器
      2. 重定位寄存器(基址寄存器, 起始物理地址)和界地址寄存器 (限长寄存器,最大逻辑地址)

内存空间扩充

覆盖技术: 解决程序大小超过物理内存总和问题.

-思想: 将程序分为多个段(模块), 常用段常驻内存,不常用段需要时调入内存.
- 内存重分为一个 **固定区域** 和若干 **覆盖区**
- 常驻固定区的,调入后不再调出直到运行结束, 不常用的段放入"覆盖区"需要时调入内存.不需要时候调出.

交换技术 : 内存空间紧张时候, 把进程短暂换出外存,把某些具备运行条件换入内存.

  • 中级调度就是要决定把那个处于挂起状态的进程重新调入内存.
  • 交换区使用连续分配方式, 比文件区速度更快.
  • PCB不会被换出内存.

内存分配和回收

连续分配方式

连续分配 :为用户进程分配的必须是一个连续内存空间.
单一连续分配方式: 内存被分为系统区和用户区. 系统区域通常在内存低地址部分,

  • 内存中只有一道用户程序
  • 优点: 实现简单,无外部碎片,采用覆盖技术扩充内存
  • 缺点: 只能单用户 单任务操作系统,有内部碎片

固定分区分配:

分区说明表 : 每个表象对应一个分区. 表项包括大小 起始位置, 状态.

动态分区分配: 根据进程大小动态建立分区.

内存记录数据结构:

  1. 空闲分区表
  2. 空闲分区链
  • 如何选择分区分配?
    需要按照一些动态分区算法.

动态分区分配没有内部碎片, 但是有外部碎片.

  • 内部碎片: 分配给某进程的内存区域里,有些部分没有用上
  • 外部碎片: 内存中某些空闲分区由于太小难以利用
    • 通过经凑技术可以解决外部碎片

动态分区分配算法:

  • 首次适应算法: 每次从低地址开始查找, 找到第一个可满足空闲分区.
  • 最佳适应算法: 动态分配是一种连续分配方法
  • 最坏适应算法: 优先使用最大的连续空闲区域.
    • 缺点: 有大进程进入可能没有空间了.
  • 邻近适应算法:
分页储存管理方式

将内存空间分为一个个大小相等的分区.

重定位寄存器: 存放装入模块存放的起始位置.

页号 = 逻辑地址 / 页面长度 (int)
页内偏移量 = 逻辑地址 % 页面长度 (余数部分)

页表 :

基本地址变换机构:
页表寄存器(PTR) :存放页表放在内存中起始位置F和页表长度M

页号是从0 开始的 ,P >= M就会发生越界中断.

例子:

具有快表的地址变换机构:

  • 局部性原理

    • 时间局部性: 如果执行了程序中某条指令,那么不久可能会再次执行。某个数据被访问过,不久后。
    • 空间局部性: 一旦程序访问了某个存储单元,不久之后附近存储单元很可能也会被访问。
  • 快表: 联想寄存器, 访问速度比内存快很多的高速缓冲存储器。

    • 内存中的页表被称为慢表。

    • 若快表命中, 访问某个逻辑地址仅需要与一次访存即可。

    • 若没有命中, 需要两次访问内存

      1. 查询页表
      2. 访问内存

两级页表
物理结构:

若采用多级页表, 各级页表大小不能超过一个页面。

基本分段管理

  • 分段

进程的地址空间: 按照程序自身逻辑关系划分成若干个段,每个段都有段名。

分页和分段管理的区别:

段页式管理

传统管理方式

  • 一次性: 作业必须一次性全部装入内存才开始工作。
    • 可能导致大作业无法运行
    • 多道程序并发度下降
  • 驻留性: 一旦作业被装入内存, 会一直驻留在内存。

虚拟内存

三个特征:

  • 多次性: 作业可以分多次调入内存
  • 对换性:作业运行中可以将作业换入换出
  • 虚拟性: 从逻辑上扩充了内存

缺页: 程序执行过程中, 如果需执行的指令或访问的数据未在内存。
-由处理器通知OS将相应的页或段调入到内存,然后继续执行程序。

实现方法

  • 请求分页系统
    • 在分页系统基础上增加请求调换页面功能和置换页面功能,置换时以页面为单位。
    • 硬件支持:请求分页的页表机制、缺页中断机构、地址变换机构。
  • 请求分段系统
    • 分段系统的基础上增加请求调段功能和分段置换功能
    • 请求分段的段表机制、缺段中断机构、地址变换机构

引入虚拟存储技术的好处:

  • 大程序: 在较小的内存里运行较大程序
  • 大的用户空间:提供给用户的可用虚拟内存通常大于物理内存
  • 并发: 内存中容纳更多的程序并发
  • 易于开发: 不会影响编程时的程序结构
请求页表机制

作用: 将用户地址空间中的逻辑地址映射为内存空间的物理地址

缺页中断处理过程:
程序在执行时,首先检查页表,当状态位指示该页不在主存时,则引起一个缺页中断发生。

缺页中断(内中断)与一般中断区别:

  • 相同点: 都是中断。需要保护现场、中断处理、恢复现场
  • 不同点:
    • 一般中断是一条指令完成后中断,缺页中断是一条指令执行时中断。
    • 一条指令执行时可能产生多个缺页中断。如指令可能访问多个内存地址,这些地址在不同的页中。

地址变换过程

页面置换算法

最佳置换算法(OPT) : 每次淘汰页面是以后永不使用或者最长时间内不再被访问的页面。
- 无法实现吹牛逼的

FIFO先进先出置换算法:每次淘汰页面都是最早进入内存的页面。
- 队列
- Belady 异常: 为进程分配物理块数增大时,缺页次数不减小反增大。
- 算法性能差

最近最久未使用置换算法(LRU): 每次淘汰页面是最近最久未使用的页面。
- 优点:性能好
- 缺点: 实现困难大、 开销大、专门的硬件

时钟置换算法(Clock)
- 最近未用算法(NRU)

- 改进时钟置换算法

页面分配策略

驻留集 : 请求分页存储管理中给进程分配的物理块合集。
- 小于进程总大小。


抖动
工作集: 某段时间间隔里,进程实际访问的页面集合

文件管理

文件系统四个层次:

  1. I/O 控制层
  2. 基本文件系统
  3. 文件组织模块
  4. 逻辑文件系统

磁盘



IO设备

按照信息交换的单位分类:
IO设备按信息交换单位分类:

  1. 块设备
  2. 字符设备

io系统主要任务:

IO系统基本功能

  1. 隐藏物理设备的细节

  2. 设备无关性(设备独立性)

    1. 用户在编制程序时,使用逻辑设备名,由系统实现从逻辑设备到物理设备(实际设备)的转换。
  3. 提高处理机和I/O设备利用率

    • 提高处理机和I/O设备利用率
    • 处理机要快速响应用户的I/O请求
    • 减少每个I/O设备运行时处理机的干预缓冲技术、虚拟设备技术
  4. 对I/O设备进行控制

    驱动设备

  5. 确保对设备的正确共享

  6. 错误处理

IO系统模型:

io控制器

组成部分:

IO控制方式

SPOOLing技术

目的:





双缓冲原理:

设置两个缓冲区buf1和buf2。读入数据时,首先输入设备向buf1填入数据,然后进程从buf1提取数据,在进程从buf1提取数据的同时,输入设备向buf2中填数据。当buf1取空时,进程又从buf2中提取数据,与此同时输入设备向buf1填数据。

程序的转换及机器级表示

知识回顾

程序、指令

计算机采取的是“存储程序”工作方式。

image.png

  • 如何工作?——程序有指令组成
    1. 程序执行前:数据和指令实现存放在存储器中,每条指令与每个数据都有地址,指令按序存放,指令由OP、ADDR字段组成,程序起始地址PC
    2. 开始执行程序
      1. 根据PC取得指令 (从五号架子取菜谱
      2. 指令译码 ( 看菜谱
      3. 取操作数 (从架子或盘中取原材料)
      4. 指令执行 (洗、切….)
      5. 回写结果(装盘或直接送桌)
      6. 改写PC值 (6 = 5 + 1 就是找下一个菜谱在哪儿)

指令执行中,指令与数据从存储器取到CPU、存放在CPU内的寄存器中,指令在IR中,数据在GRP.

指令要给出的数据:

  • 操作性质: 操作码
  • 源操作数1 或/和 源操作数2 (立即数、寄存器编号、存储地址)
  • 目的操作数地址(寄存器编号、存储地址)
  • 存储地址的描述与操作数的数据结构有关

不同层次语言之间的等价转换

image.png

程序转化概述

指令

计算机中的指令包括:微指令机器指令以及伪(宏)指令之分。

  • 机器指令: 处于硬件和软件的交界面,相当于一个菜谱指定的一个完整做菜过程。

[!NOTE]
本章中提到的的指令都是 机器指令

  • 微指令: 是指微程序级别指令,属于硬件范畴。相当于洗、切、煮等做菜的微过程
  • 伪指令:若干机器指令组成的指令序列,属于软件范畴。相当于多个菜谱合成的一个大菜谱。
  • 汇编指令: 是机器指令的汇编表示形式,即符号表示。。
    • 机器指令与汇编指令一一对应,都与具体机器结构有关,属于机器级别指令

机器级指令

机器指令是一个0/1序列,由若干自度那组成。

image.png

汇编指令是机器指令的符号表示(允许有不同的格式):

image.png

b是一个长度后缀表示一个字节。

指令功能为: M[R[bx] + R[di] - 6] <- R[cl]。
这就是一个寄存器传送语言RTL(Register Transfer Language)

  • R: 寄存器内容

  • M: 存储单元内容

    • 也可以用(x), 表示地址x中内容。
      mov、bx、movb、%bx都是助记符
  • 用GCC编译器套件进行转换的过程
    image.png

  1. 预处理:在高级语言源程序中插入所有用#include命令指定的文件和用#define声明指定的宏。
  2. 编译:将预处理后的源程序文件编译生成相应的汇编语言程序。
  3. 汇编:由汇编程序将汇编语言源程序文件转换为可重定位的机器语言目标代码文件。
  4. 链接:由链接器将多个可重定位的机器语言目标文件以及库例程(如printf()库函数)链接起来,生成最终的可执行目标文件。
  • GCC使用举例:
  1. gcc -E xxx.c -o xxx.i 生成预处理的文件
  2. gcc -S xxx.c -O xxx.s 生成汇编源程序
    image.png
  • 后面更了 l、w等字长符号。
  1. gcc -c test.s -o test.o 生成可重定位目标程序
  2. objdump -d test.o反汇编(与源代码生成的汇编不同):
    image.png
  3. objdump -d test 反汇编可执行文件
    • 与可重定位的反汇编相比,他的地址就不是从0开始,是从真实地址开始。
      image.png

可执行文件的存储映像

image.png

回顾ISA

ISA(Instruction Set Architecture)位于软件和硬件之间

ISA规定如何使用硬件:

  • 可执行的指令的集合,包括指令格式、操作种类以及每种操作对应的操作数的相应规定;
  • 指令可以接受的操作数的类型
  • 操作数所能存放的寄存器组的结构,包括每个寄存器的名称、编号、长度和用途
  • 操作数所能存放的存储空间的大小和编址方式
  • 操作数在存储空间存放时按照大端还是小端方式存放
  • 指令获取操作数的方式,即寻址方式
  • 指令执行过程的控制方式,包括程序计数器、条件码定义等。

ISA与计算机组成(微结构)之间关系:不同ISA规定的指令集不同,例如:IA-32,MIPS、ARM。计算机组成必须要能实现ISA规定的功能。例如GRP、标志等。

IA-32

image.png

在IA-32体系中,有8个GRP(0-7)通用寄存器,一个EFLAGs 标志寄存器、程序计数器PC为EIP。

可寻址的空间是4GB, (编号为0 ~ 0xFFFFFFFF)
指令格式变长(长度可变)、操作码变长、指令由若干字段(OP、Mod、SIB等)组成

计算机中的数据存放在寄存器文件、通用寄存器组GRPs. (宿舍书架)、存储器。

IA-32 支持的数据结构

主要是定点书与浮点数:

IA-32 架构有16位架构发展而来,因此虽然字长32位火更大、但是一个字16位,长度后缀位w, 32 位为双字,长度为I。 long double 实际长度为80位,但是分配96位 = 12B按照4B对齐。

寄存器组织

image.png

IA-32 有

  1. 8个通用寄存器(32位的分别为:EAX、EBX、…..)
  2. 两个专用寄存器 EIP程序计数器和EFLAGs 标志寄存器。
  3. 6个段寄存器
    但是IA-32是通过扩展方式实现的。
  • 8个8位寄存器(AH、AL、BH、BL、…..)

  • 8个16位寄存器(AX、BX……)

  • IA-32 标志寄存器
    6个条件标志:

  • OF、SF、ZF、CF ([[计算机系统基础#n位加法器]])

  • AF: 辅助进位标志(BCD码运算有意义)

  • PF:奇偶标志

3个控制标志:

  • DF: 方向标志(自动变址方向是增还是减)
  • IF: 中断允许标志

寻址方式

寻址方式:如何根据指令给定信息得到操作数或者操作数地址。

操作数所在位置:

  • 指令中:立即寻址
  • 寄存器中:寄存器寻址
  • 存储单元: 其他寻址方式

存储器操作数寻寻址方式与微处理器工作模式有关:实地址模式(基本用不到)与保护模式

  • 实地址模式:寻址空间为1MB, 20位地址: CS << 4 + (IP)

  • 保护模式: 加点进入后、采用虚拟存储管理方式。多任务情况下隔离、保护。
    寻址空间: 232B = 4G , 32 位线性地址分段(段基址+ 段内偏移量)

保护模式下的寻址方式:

image.png

寻址举例子:

1
2
3
4
5
int x;
float a[100];
short b[4][4];
char c;
double d[10];

image.png

double 对齐:

  • Linux 按 4B 边界对齐

  • windows 按8B边界对齐

  • a[i] 的地址如何计算?

    • 104 + i*4 (因为是float ,4Byte)
    • i = 99时, 104+99x4 = 500
  • b[i][j]地址如何计算:

    • 504 + i * 8(i * 8 是因为 一个short 是2Byte。然后按照一个行有4个short 4 * 2 = 8)
  • d[i] 地址计算: 544 + i * 8

    • 为什么起始是 544 而不是 537 因为是8B对齐的。要能被8整除。

其他变量寻址方式:

  1. x、c:位移 / 基址
  2. a[i]:104+i×4,比例变址+位移
  3. d[i]:544+i×8,比例变址+位移
  4. b[i][j]: 504+i×8+j×2,基址+比例变址+位移

算完地址,我们可以举例将b[i][j]取到AX中的指令是:

1
2
movx 504(%ebp, %esi, 2), %ax
// i x 8 在EBP中, j在esi中,2 为比例因子

机器指令格式

image.png

  • 位移量立即数都可以是1B、2B、4B
  • SIB中基址B和变址I 都可是 8个GRS中任意一个。SS给出比例因子(数字1、2、4、8 因此只需要两位)
  • 操作码(1或2个字节):opcode ;w :与机器模式(16/32位)一起确定寄存器的位数(AL/AX/EAX)D操作方向(确定源和目标)
  • 寻址方式(ModRM字节): mod, r/m ,reg/op三个字段与w字段和机器模式(16/32)一起确定操作数所在寄存器编号或有效地址的计算方式。

image.png

常用指令类型

  1. 传送指令

    • 通用数据传送指令
      • MOV:一般传送,包括movb、movw和movl等
      • MOVS:符号扩展传送,如movsbw、movswl等
      • MOVZ:零扩展传送,如movzwl、movzbl等
      • XCHG:数据交换
      • PUSH/POP:入栈/出栈,如pushl,pushw,popl,popw等
    • 地址传送指令
      • LEA:加载有效地址,如leal (%edx,%eax), %eax”的功能为R[eax]←R[edx]+R[eax],执行前,若R[edx]=i,R[eax]=j,则指令执行后,R[eax]=i+j
    • 输入输出指令
      • IN和OUT:I/O端口与寄存器之间的交换
    • 标志传送指令
      • PUSHF、POPF:将EFLAG压栈,或将栈顶内容送EFLAG
  2. 定点算术运算指令

    • 加 / 减运算(影响标志、不区分无/带符号)
      • ADD:加,包括addb、addw、addl等
      • SUB:减,包括subb、subw、subl等
    • 增1 / 减1运算(影响除CF以外的标志、不区分无/带符号)
      • INC:加,包括incb、incw、incl等
      • DEC:减,包括decb、decw、decl等
    • 取负运算(影响标志、若对0取负,则结果为0且CF=0, 否则CF=1)
      • NEG:取负,包括negb、negw、negl等
    • 比较运算(做减法得到标志、不区分无/带符号)
      • CMP:比较,包括cmpb、cmpw、cmpl等
    • 乘 / 除运算(区分无/带符号)
      • MUL / IMUL:无符号乘 / 带符号乘(影响标志OF和CF)
      • DIV/ IDIV:无符号除 / 带符号除

栈操作

栈(Stack)是一种采用“先进后出”方式进行访问的一块存储区,用于嵌套过程调用。从高地址向低地址增长

[!NOTE]
栈不等于堆栈(堆栈由“堆”和“栈”组成)

  • 入栈(pushw %ax)
    要把AL这一低数据放在低地址(小端).
    0xFFFFH是栈地。
    image.png

入栈前: R[sp] <- R[sp] - 2、M[R[sp]] <- R[ax] ()

  • 出栈(popw %ax)

汇编操作:R[ax] <- M[R[sp]]、[sp] <- R[sp] + 2

整数乘除指令

  • 乘法指令:可给出一个、两个或三个操作数
    • 给出一个操作数SRC,则另一个源操作数隐含在AL/AX/EAX中,将SRC和累加器内容相乘,结果存放在AX(16位)或DX-AX(32位)或EDX-EAX(64位) 中。DX-AX表示32位乘积的高、低16位分别在DX和AX中。 n位× n位=2n位
    • 若指令中给出两个操作数DST和SRC,则将DST和SRC相乘,结果在DST中。n位× n位=n位
    • 若指令中给出三个操作数REG、SRC和IMM,则将SRC和立即数IMM相乘,结果在REG中。n位× n位=n位
  • 除法指令:只明显指出除数
    • 若为8位,则16位被除数在AX寄存器中,商送回AL,余数在AH
    • 若为16位,则32位被除数在DX-AX寄存器中,商送回AX,余数在DX
    • 若为32位,则被除数在EDX-EAX寄存器中,商送EAX,余数在EDX

以上内容不要死记硬背,遇到具体指令时能查阅到并理解即可

举例子

  • 无符号乘法:
    image.png
  • 有符号乘法(布斯乘法):

image.png

  • 三个操作数乘法:
    image.png

按位运算指令

  • 逻辑运算(仅NOT不影响标志,其他指令OF=CF=0,而ZF和SF根据结果设置:若全0,则ZF=1;若最高位为1,则SF=1 )
    • NOT:非,包括 notb、notw、notl等
    • AND:与,包括 andb、andw、andl等
    • OR:或,包括 orb、orw、orl等
    • XOR:异或,包括 xorb、xorw、xorl等
    • TEST:做“与”操作测试,仅影响标志
  • 移位运算(左/右移时,最高/最低位送CF)
    • SHL/SHR:逻辑左/右移,包括 shlb、shrw、shrl等
    • SAL/SAR:算术左/右移,左移判溢出,右移高位补符(移位前、后符号位发生变化,则OF=1 )
    • ROL/ROR: 循环左/右移,包括 rolb、rorw、roll等
    • RCL/RCR: 带循环左/右移,将CF作为操作数一部分循环移位

以上内容不要死记硬背,遇到具体指令时能查阅到并理解即可

举例子:

假设short型变量x被编译器分配在寄存器AX中,R[ax]=FF80H,则以下汇编代码段执行后变量x的机器数和真值分别是多少?
movw %ax, %dx
salw $2, %ax
addl %dx, %ax
sarw $1, %ax

解:$2和$1分别表示立即数2和1 。
x是short型变量,故都是算术移位指令,并进行带符号整数加。
假设上述代码段执行前R[ax]=x,则执行((x<<2)+x)>>1后,R[ax]=5x/2。算术左移时,AX中的内容在移位前、后符号未发生变化,故OF=0,没有溢出。最终AX的内容为FEC0H,解释为short型整数时,其值为-320。验证:x=-128,5x/2=-320。经验证,结果正确。

控制转移指令

  • 指令执行可按顺序 或 跳转到转移目标指令处执行

    • 无条件转移指令
      • JMP DST:无条件转移到目标指令DST处执行
    • 条件转移
      • Jcc DST:cc为条件码,根据标志(条件码)判断是否满足条件,若满足,则转移到目标指令DST处执行,否则按顺序执行
    • 条件设置
      • SETcc DST:将条件码cc保存到DST(通常是一个8位寄存器 )
    • 调用和返回指令 (用于过程调用)
      • CALL DST:返回地址RA入栈,转DST处执行
      • RET:从栈中取出返回地址RA,转到RA处执行
  • 条件转移指令的分类:

    1. 根据单个标志的值转移
    2. 无符号整数比较转移
    3. 带符号整数比较转移

image.png

数据的机器级表示与处理

浮点数的表示

  • 浮点数的规格化表示是唯一的:小数点前只有一个非零数

只要对尾数和指数分别编码,就可以表示一个浮点数(实数)。

32位浮点数格式的规格化数表示范围:

1
+/- 0.1xxxx * 2^E 
  • 第0位:符号位
  • 第1-8位:表示阶码,偏置常数 128,
  • 第9-31位:小数的尾数,规格化尾数的小数点后一位总是1(二进制下),因此可以省去,用23为表示24个数字
    • 最大正数: 0.1111….1 * 211…1 = (1-2-24) * 2 127
      • 0.1 = 2-1
        1. 00000 - 0.000…1 = 0.11…..1
      • 偏置常数 + 127 = 255 = 28 - 1
    • 最小正数: 0.100…0 * 200….0 = (2-1 )* 2-128 = 2-129
      • 为什么是0.100..0?
      • 因为小数点之后的1 是被省略的1

综上所述:

机器0: 尾数全部为0的时候,或者落在下溢区域的数。

浮点数比定点数表示的范围更大,但是数的个数没有变多,因此更为稀疏了。

浮点数的规格化

现在所有通用计算机使用 IEEE-754表示浮点数

  • 规格化数的形式: +/- 1.xxxx * R^Exponent

  • 单精度浮点数表示格式:

  • Exponent(阶码 / 指数编码):

    • SP规格化数阶码范围为0000 0001 (-126) ~ 1111 1110 (127)
      • 全1 或 全0 用来表示特殊值
    • bias为 127 (single), 1023 (double)
    • 全部为0时可以看作 -127, 注意此时的偏置常数为127
  • Significand(尾数):

    • 规格化尾数最高位总是1,所以隐含表示,省1位

所以单精度表示的真正的值为: (-1)S x (1 + Significand) x 2Exponent-127

例题:已知单精度浮点数-12.75 ,求在机器中表示的数.

  1. 负数,因此 S为1
  2. 12 = 8 + 4 = 1100B
  3. 0.75 计算方法:
    1. 0.75 * 2 = 1.50 进一
    2. 0.50 * 2 = 1.00 进一
    3. 得到 11B
  4. 12.75 = 1100.11B = 1.10011 x 23
  5. 127 + 3 = 1000 0010 B
  6. 最终结果:
  7. 省去了小数点前面的 1

特殊数字表示

  • 0 的表示
    • -0 : 1 0000 0000 0000 0000 0000 0000 000
    • +0:0 0000 0000 0000 0000 0000 0000 000
  • +/- ♾️
      • 0 11111111 00000000000000000000000
      • :1 11111111 00000000000000000000000
    • 浮点数 / 0 得到的值是♾️ inf
  • 非数: Not a Number(NaN)
    • Exponent 全一, Singnificand非0
    • 例如 sqrt(-4.0)

非规格化数的表示

由上图的上半部分可以看到,2-126 ~ 2-125 中的刻度要比 2-125到2-124更密集。为啥?

[!INFO]
指数位的值加1,相当于将浮点数乘以2,而指数位的值减1,相当于将浮点数除以2。因此,在指
数位相同的情况下,相邻的两个浮点数之间的差值是2的幂次方。

对于2-126到2-125中的每个浮点数,它们的指数位都为-126,因此它们之间的差值是2的-23次幂,即2-126乘以2的-23次幂,约为1.18 x 10-38。而对于2-125到2-124中的每个浮点数,它们的指数位都为-125,因此它们之间的差值是2的-23次幂乘以2,即2-125乘以2的-23次幂,约为2.35 x 10-38。因此,2-126到2-125中的刻度比2-125到2-124更密集。

GAP 区域则是下溢区域, 为了更好利用这部分数字,我门可以使用非规格化表示。

(-1)s * 0.xxxx * 2-128

其他数据的编码

  • 逻辑字符
    逻辑值只有:真或假。

逻辑值的表示用二进制的一位来表示,0 为假 1为真。

  • 西文字符编码

ASCII码来表示。
字符串的操作: 传送比较

  • 汉字及国际字符编码表示

特点: 表意文字、是个方块图形、数量巨大

编码形式:

  1. 输入 ——输入码: 对汉字用相应按键编码
  2. 查找传输处理——内码用于在系统中存储、查找、
  3. 打印显示——字模点阵或轮廓描述

数据宽度与存储

数据的基本宽度

在计算机中最小的处理、存储、传输信息的最小单位是——比特(bit)
二进制信息最基本的计量单位是“字节”(Byte)

  • 现代计算机存储器按字节编址
  • 字节是最小可寻址单位
  • LSB: 小端序 MSB:大端序

在实际使用中,我们也用字来作为单位:
IA-32 中 字: 16位 、 字长:32位

字是一中体系结构如 IA-32规定长度, 字长是数据通路的宽度 ,也就是寄存器、数据传输..的宽度。

  • 数据通路:CPU内部数据流经的路径以及路径上的部件,主要是CPU内部进行数据运算、存储和传送的部件,这些部件的宽度基本上要一致,才能相互匹配
  • 字:表示被处理信息的单位,用来度量数据类型的宽度。

[!INFO] x86体系结构定义“字”的宽度为16位,但从386开始字长就是32位

数据量的度量单位

储存二进制信息是度量单位要远大于字节或字。

  • KB 千字节 1KB = 210字节 = 1024B
  • MB 兆字节 1MB = 220字节 = 1024KB
  • GB 千兆字节 1GB = 230字节 = 1024MB
  • ……

速率单位:

注意是bit, 我们常说的MB/s,是千字节每秒

  • “千比特/秒”(kb/s),1kbps=103 b/s=1000 bps
  • “兆比特/秒”(Mb/s),1Mbps=106 b/s =1000 kbps
  • “千兆比特/秒”(Gb/s),1Gbps=109 b/s =1000 Mbps
  • “兆兆比特/秒”(Tb/s),1Tbps=1012 b/s =1000 Gbps

程序中的数据类型宽度

  • C语言中的数值类型宽度
    image.png

Compaq Alpha 是一个针对高端应用的64位机器,字长64

数据的存储和排列方式

在计算机中,一个基本数据可能占用多个存储单元。如 int x = 10 x的存放地址为100, 机器数为FFFFFFF6H 占用四个单元。

那么在计算机中给出存放地址一般是最小地址,即x总共占用 100~103 这个四个单元。那么这四个单元内数据的存放顺序就是我们要说的大端序/小端序

左侧即位小端序,右侧为大端序。
大端序符合人的阅读顺序,小端序则是逆序。

大端序(Big Endian): MSB 所在的地址是数的地址
小端序(Little Endian): LSB 所在的地址是数的地址
image.png

C语言中 union 存放顺序是所有成员是从低地址开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
union NUM{
int a;
char b;
} num;
num.a = 0x12345678;
if(num.b == 0x12)
printf("Big Endian \n");
else
printf("Little Endian \n");
printf("num.b = 0x%X\n", num.b);
return 0;
}

字节交换问题

如果一个是大端序、一个是小端序在交换数值是就会发生一些小问题。

数据的表示和运算

按位运算和逻辑运算

按位运算: 对位串实现“掩码”(mask) 操作或相应的其他处理。(主要用于多媒体数据或状态\控制信息处理)。

  • 主要操作:

    • 按位或:“|”
    • 按位与:“&”
    • 按位取反:“~”
    • 按位异或:“^”
  • 逻辑运算: 用于关系表达式的运算。

    • 或:||
    • 与:&
    • 非:!

与按位运算不同的是,逻辑运算符针对的是一个整体。结果类型是了逻辑值。

移位运算与位截断/扩展运算

移位运算:

移位运算主要用于扩大和缩小数值的2、4、8….倍。

  • 操作:
    • 左移: x<<k;
    • 右移: x>>k;

在进行移位运算是保持着

  • 无符号数:高(低)位移出,低(高)位补0
  • 有符号数:
    • 左移:高(低)位移出,低(高)位补0
    • 右移:低位移出,高位补符,可能发生有效数据丢失
      的规则, 也就是说有可能会导致溢出数据丢失

位截断/扩展运算
C语言中没有专门操作运算符,但是在类型转换是会实际发生。

例如在这一段代码中:

1
2
3
int i = 32768;
short si = (short) i;
int j = si;

i 的所占单元要大于short类型,因此在转化为 short时,si等于的是i被截断的值:-32768。
j 作为 int 类型,从short转化为 int 会发生扩展运算,但是值实际没有发生变化, j = -32768

变化过程:

  1. i = 32768 00 00 80 00
  2. si = -32768 80 00
  3. j = -32768 FF FF 80 00

因此从上面我们可以发现截断与扩展的规则:

  • 截断: 强行将高位丢弃,故可能发生“溢出”
  • 扩展:
    • 无符号数:0扩展,前面补0
    • 带符号整数:符号扩展,前面补符

算术运算

讲完了基本的运算规则,那么计算机如何是实现高级语言中的运算呢?

在前面中,我们学习了补码。
那么根据补码的定义:

${[X]}_补 = 2^n + X (-{2}^{n-1} \leq X \le 2^{n-1}, mod 2^n)$

我们可以发现有如下公式:

[!INFO]
[x+y]=2n+x+y= 2n+x+2n+y= [x]+[y] (mod 2n)
[x-y]=2n+x-y= 2n+x+2n-y= [x]+[-y] (mod 2n)
[-x] = $\overline{\text{[x]}}$ + 1

上面的第三个式子就是实现减法的主要工作

利用带标志加法器,可构造整数加/减运算器,进行以下运算:

  • 无符号整数加、无符号整数减
  • 带符号整数加、带符号整数减

这也是为什么我们说,减法是由加法来实现的。

一位加法器——全加器

image.png

如图一位加法器又叫全加器。符号为FA (Fulling Adder)
两个加数为A与B, 低位进位为 Cin ,和为 F ,向高位进位Cout.

运算逻辑表达式:
${F = A \oplus B \oplus Cin }$
${Cout = A \land B + A \land Cin + B \land Cin }$

image.png

n位加法器

n位全加器可以通过使用 n 个全加器实现。

让我们来欣赏一下加法器的美图吧,家人们:

image.png

我们将 Cout 即全加器的高位进位进行串联,作为下一个全加器的 Cin
是一个逻辑门电路。

如下图连接方式:
image.png

其他所有算术运算部件基于加法器。

图上的基本加法器无法用于两个 n 位带整数(补码) 加减。因为无法判断是否溢出。

  • N位带标志加法器

在编程之中,我们需要经常比较大小, 原理是我们通过底层的加法器做减法得到的标志信息判断的。

下面就是N位带标志加法器的果图,十分的SEXY:

image.png

image.png

  • 标志含义:
    • OF:溢出标志
      • ${OF = C_m \oplus C_{m-1}}$
    • SF: 符号标志
      • ${SF = F_{n-1}}$
    • ZF: 0 标志
      • ZF = 1 当且仅当 F = 0即结果为0 时
    • CF:进位\借位标志
      • ${CF = Cout \oplus Cin}$

那么我们学习了n位带标志的加法器,我们是如何计算的呢?

在前文中,提到了补码的规则。负数的补码是其正数补码尾数+1的值。
因此在计算 A - B 时,实际上计算的是:
[A-B]=2n+A-B= 2n+A+2n-B= [A]+[-B] (mod 2n)

image.png

通过**多路选择器(Mux)**,来选择B输出的是原码还是补码。
当 Sub 为-1,就是做减法,因此我们要使用补码。反之亦然。

那么我们在上面的基础上添加寄存器、移位器以及逻辑控制就可以实现ALU、乘/除以及浮点运算电路

  • ALU
    image.png

可以进行基本的算术运算与逻辑运算,核心电路是带标志位加法器

  • 如何判断结果输出的是针对的哪一种运算?
  • 是看标志ALUop,它的位数决定操作种类。当她有三位时,就有 8 种操作。

整数的加减运算

在计算机中,指针、地址等通常为无符号整数,因此在进行相关操作是要进行无符号整数加减。

无符号整数与带符号整数加减运算电路完全一样, 称之为整数加减运算部件。基于带标志加法器

[!INFO]

  1. 计算机所有运算基于加法器
  2. 加法器不知道运算是带符号数还是不带符号数
  3. 加法器不判断对错,总是取低n为作为结果,生成标志信息
  • 条件标志位:在运算电路中产生,被记录到专门的寄存器。
    • 被称为 程序/状态字寄存器标志寄存器
      image.png

在做加法时判断是否溢出:

  • 无符号整数: CF = 1
  • 带符号整数: OF = 1

在做加法时判断是否溢出:

  • 无符号整数: CF = 1
  • 带符号整数: OF = 1

利用减法判断两数大小:

  • 无符号整数: CF = 0 (减法借位标识),则 A大于B
  • 有符号整数 OF = SF (溢出标志 = 符号标志)的时候,A大于B

减法

n是代表是几位数字。

无符号减公式:(小于零是要模,因为溢出了)

$$
result = \left{ \begin{aligned} x - y &&(x-y>0) \ x - y + 2^n && (x - y < 0) \end{aligned} \right.
$$

带符号减公式:

$$
result = \left{ \begin{aligned} x - y - 2^n &&(2^{n-1} \le x-y) && 正溢出\ x - y &&(-2^{n-1} \le x-y \lt 2^{n-1}) && 正常\ x - y + 2^n && (x - y < -2^{n-1}) && 负溢出 \end{aligned} \right.
$$

加法

无符号加公式

$$
result = \left{ \begin{aligned} x + y &&(x+y \le 2^n) \ x - y + 2^n && (2^{n} \le x + y \lt 2^{n+1}) \end{aligned} \right.
$$

带符号减公式:

$$
result = \left{ \begin{aligned} x + y - 2^n &&(2^{n-1} \le x+y) && 正溢出\ x + y &&(-2^{n-1} \le x+y \lt 2^{n-1}) && 正常\ x + y + 2^n && (x + y < -2^{n-1}) && 负溢出 \end{aligned} \right.
$$

整数乘除法

整数乘法

高级语言中, 两个n位数乘积的会得到 2n位 的数字,但是我们只取低n位。

在计算机内部,x2带符号整数不一定是正数(溢出成负值),但是浮点数一定是正数。

例如,四位的带符号整数,x = 5,会发生溢出。 计算过程: image.png

由于是两个4位带符号整数相乘会得到一个8位的整数,我们取低四位即 1001 他的真值就是 -111 ,因此就是 -7。

image.png

假定两个n位无符号整数xuyu对应的机器数为XuYupu=xu×yupu为n位无符号整数且对应的机器数为Pu; 两个n位带符号整数xsys对应的机器数为XsYsps=xs×ysps为n位带符号整数且对应的机器数为Ps。 若Xu=XsYu=Ys,则Pu=Ps

-溢出判断: - 无符号:若Puh=0,则不溢出 - 带符号:若Psh每位都等于Ps的最高位,则不溢出

通过X*Y的高n位可以用来判断溢出 无符号:若高n位全0,则不溢出,否则溢出 带符号:若高n位全0或全1且等于低n位的最高位,则不溢出。

image.png

整数除法

手算二进制除法:
image.png

其实与一般除法无异。

在带符号的整数,n 位整数除n位整数,除 -2n-1 / - 1 = 2n-1 发生溢出。

商的绝对值不可能比被除数的绝对值更大。

整数除法其商也是整数,随意在无法整除是要舍入。
通常下,是按照0方向舍入。
整数除0的结果无法用机器数表示, 会出现异常。

我们举个例子:

1
2
3
4
int a = 0x80000000
int b = a / -1
printf("%d\n",b)

运行结果是: -2147483648。
通过 Objdump反汇编可以得知, -1 被优化为了取负指令neg ,因此没有发生除法溢出。
但是,结果是错误的,0x80000000 是 -231 , 结果应该是 231 . 但是由于32位的补码无法表示,因此溢出。

1
2
3
4
5
int a = 0x80000000;
int b = -1;
int c = a / b;
printf("%d\n",c)

结果是 Float Point Exception. 其实就是编译器底层无法优化为取反指令。

变量与常数之间的运算

计算机中除法运算更为复杂,因此不能使用流水线方式,一次除法需要消耗30多个或者更多始终周期。

为了缩短时间,编译器在处理一个变量与一个2的幂次形式的整除时,经常才去右移运算实现。

在可以整除的时候可以直接右移。

  • 无符号:逻辑右移
    • 12/4 = 3 = 0000 1100 >> 2 = 0000 0011
  • 有符号:算术右移
    • -12/4 = 3 = 1111 0100 >> 2 = 1111 1101

不能整除时,右移移出的位中有非0,需要处理。

  • 不能整除时,采用朝0舍入,即截断处理。
    • 无符号数、带符号正整数:移出低位之后直接丢弃。
      • 举例:14 /4 = 3 = 0000 1110 >> 2 = 0000 0011
    • 带符号负整数:加偏移量(2k - 1) 然后右移k位,低位截断。k是右移位数。
    • ❎直接截断: -14 / 4 = -4 = 1111 0010 >> 2 = 1111 1100 = -4
    • 纠偏右移: k = 2 (-14 + 22 - 1) / 4= -3 = 1111 0101 >> 2 = 1111 11101 = -3

浮点数运算及结果

<<<<<<< HEAD
image.png

可能会有几种情况:

  • 阶码上溢:一个正指数超过最大允许值 => +∞/-∞ /溢出
  • 阶码下溢:一个负指数超过最小允许值 => +0/-0
  • 尾数溢出: 最高有效位有进位 => 右规
    • 1.5 + 1.5 = 11.00….000 但是可以右移将非规格化数合规
  • 非规格化位数:数值部分高位为0 => 左规
    • 1.5 - 1.0 = 0.10…0 尾数要左移,阶码减1
  • 右规或对阶

IEEE规定的五种异常情况

  1. 无效运算
    1. 运算时有一个是非有限数 (♾️)
    2. 结果无效: NaN 。。。。
  2. 除以0 (无限大)
  3. 数太大(阶上溢)
  4. 数太小(阶下溢)
  5. 结果不精确如 1 / 3 无法精确表示

浮点数目的运算过程可以大致分为一下几个过程:

1.求阶差: ∆e=Ye – Xe (若Ye > Xe,则结果的阶码为Ye)

2. 对阶: 将两个小数的阶码对齐,小阶向大阶对齐

  • 举个例子: 1.123 x 105 + 2.230 x 103 = 1.123 x 105 + 0.02230 x 105

IEEE 754尾数右移时,要将隐含的“1”移到小数部分,高位补0,移出的低位保留到特定的“附加位”上

方便我们来计算。

3. 尾数加减: Xm*2Xe-Ye ± Ym

4. 规格化:

  • 当尾数高位为0,则需左规:尾数左移一次,阶码减1,直到MSB为1或阶码为00000000(-126,非规格化数)
  • 每次阶码减1后要判断阶码是否下溢(比最小可表示的阶码还要小)
  •   当尾数最高位有进位,需右规:尾数右移一次,阶码加1,直到MSB为1
    
  • 每次阶码加1后要判断阶码是否上溢(比最大可表示的阶码还要大)

阶码溢出异常处理:阶码上溢,则结果溢出;阶码下溢到无法用非规格化数表示,则结果为0

5. 若尾数比规定位数长(有附加位),则考虑舍入
6. 若尾数结果为0, 结果也应该为0

附加位

为了保证每次运算尽可能不丢失精度,我们所想出来的办法是——附加位。

IEEE 754: 规定中间结果必须在右边加两个附加位。

  1. Guard 保护位:在Significand右边的位
  2. Round 舍入位:在保护位右边的位

作用:用以保护对阶时右移的位或运算的中间结果。
处理: ①左规时被移到significand中; ② 作为舍入的依据。

举例子:image.png
默认舍入方式:就近舍入。

image.png

Z1和Z2分别是结果Z的最近的可表示的左、右两个数 )

  1. 就近舍入(精度最高):舍入为最近可表示的数
    非中间值:0舍1入;
    中间值:强迫结果为偶数-慢

举例:
附加位为
01:舍
11:入
10:(强迫结果为偶数)
2. 朝+∞方向舍入:舍入为Z2(正向舍入)
3. 朝-∞方向舍入:舍入为Z1(负向舍入)
4. 朝0方向舍入:截去。正数:取Z1; 负数:取Z2

image.png

  • C语言中float和double分别对应IEEE 754单精度浮点数格式和双精度浮点数
  • long double类型的长度和格式随编译器和处理器类型的不同而有所不同,IA-32中是80位扩展精度格式
  • 从int转换为float时,不会发生溢出,但可能有数据被舍入
  • 从int或 float转换为double时,因为double的有效位数更多,故能保留精确值
  • double转换为float和int时,可能发生溢出,此外,由于有效位数变少,故可能被舍入
  • float 或double转换为int时,因为int没有小数部分,所以数据可能会向0方向被截断

范围与精度

float型表示范围:
最大数据: +1.11…1 x 2127 约为 +3.4x1038

大数吃小数

这个是精度问题所导致的:

image.png

在程序之中,我们总是先计算括号部分,因此在第二行的中,1.0在对阶中就被省略成0了。

SpringCloudAlibaba

Alibaba 的实现:

  • 流量控制和服务降级:使用阿里巴巴Sentinel进行流量控制,断路和系统自适应保护
  • 服务注册和发现:实例可以在阿里巴巴Nacos中注册,客户可以使用Spring管理的bean发现实例。通过Spring Cloud Netflix支持Ribbon,客户端负载均衡器
  • 分布式配置:使用阿里巴巴Nacos作为数据存储
  • 事件驱动:构建与Spring Cloud Stream RocketMQ Binder连接的高度可扩展的事件驱动微服务
  • 消息总线:使用Spring Cloud Bus RocketMQ链接分布式系统的节点
  • 分布式事务:支持高性能且易于使用的Seata分布式事务解决方案
  • Dubbo RPC:通过Apache Dubbo RPC扩展Spring Cloud服务到服务调用的通信协议

Dashboard :

  • 默认用户名 nacos
  • 默认 密码 nacos

官方文档: https://github.com/alibaba/spring-cloud-alibaba/blob/2022.x/README-zh.md

版本说明: https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

  • Mavne 依赖导入
  • 最新版本Spring Cloud 要求JDK>=17
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.9.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

springBoot 2.x

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.10-RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

可以直接用Docker 拉去

1
docker pulll nacos/nacos-server

本次教程全部采用 2.2.10-RC1

Nacos 服务注册与配置中心

  • 替换eureka config
  • Naming Configuration

开源地址: https://github.com/alibaba/Nacos

Discovery

  • 引入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
  • 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    server:  
    port: 9001 # server port

    spring:
    application:
    name: nacos-payment-provider
    cloud:
    nacos:
    discovery:
    server-addr: 127.0.0.1:8848 # nacos 地址
    management:
    endpoints:
    web:
    exposure:
    include: '*'

注意: 如果不想使用Nacos作为注册发现可以将spring.cloud.nacos.discovery设置为false

  • 启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * @author re1ife
    * @data 2023-02-25 16:11:48
    */
    @SpringBootApplication
    @EnableDiscoveryClient
    public class PaymentMain9001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentMain9001.class, args);
    }
    }

  • controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController  
    public class PaymentController {
    @Value("${server.port}")
    private String serverport;

    @GetMapping(value = "/payment/nacos/{id}")
    public String getPayment(@PathVariable("id") Integer id){
    return "nacos registry ,server port: "+ serverport +"\t id"+id;
    }
    }
  • 重复建造一个payment9002

  • 服务名不同情况:

  • 相同:

消费者注册和负载均衡

  • Nacos 自动ribbon负载均衡
  1. 消费者模块化创建
  2. POM依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    <?xml version="1.0" encoding="UTF-8"?>  
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent> <groupId>com.re1ife.atiguifu_springcloud</groupId>
    <artifactId>cloud2020</artifactId>
    <version>1.0</version>
    </parent>
    <artifactId>cloudalibaba-consumer-nacos-order83</artifactId>

    <properties> <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties> <dependencies> <!--SpringCloud ailibaba nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
    <groupId>com.re1ife.atiguifu_springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0</version>
    </dependency> <!-- SpringBoot整合Web组件 -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency> <dependency> <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency> <!--日常通用jar包配置-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
    </dependency> <dependency> <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency> <dependency> <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency> </dependencies></project>
  • yaml 配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    server:  
    port: 8083


    spring:
    application:
    name: nacos-order-consumer
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848


    #消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
    service-url:
    nacos-user-service: http://nacos-payment-provider

上面的Service-url 对应

1
2
//Eureka写法
public static final String SERVER_URL = "http://nacos-payment-provider";
  • 消费者启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableDiscoveryClient //开启服务发现
    public class ConusmerNacosMain83 {
    public static void main(String[] args) {
    System.out.println("Hello world!");
    }
    }
  • 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration  
    public class ApplicationContextConfig {
    @Bean
    //LoadBalanced 会自动解析服务名的url
    @LoadBalanced
    public RestTemplate getRestTemplate(){
    return new RestTemplate();
    }
    }
  • controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.re1ife.atiguifu_springcloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

/**
* @author re1ife
* @data 2023-02-26 13:33:54
*/

@RestController
@Slf4j
public class OrderNacosController {

@Resource
private RestTemplate restTemplate;

@Value("${service-url.nacos-user-service}")
private String serviceURL;

@GetMapping("/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id){
return restTemplate.getForObject(serviceURL+"/payment/nacos/"+id,String.class);
}


}

注册中心对比

  • nacos 支持 AP或CP
  • C : 数据强一致性
  • A: 高响应
  • P分区容忍:系统中任意信息的丢失或失败不会影响系统的继续运作

服务配置中心

  • 出现背景
    微服务意味着将单体应用中业务拆分成子业务, 颗粒度变小, 存在大量配置信息, 因此要集中管理配置服务。

  • 服务端和客户端

    • 服务端:分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
    • 客户端:通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器 (默认git)
  • 替代Config

    Nacos 和 SpringCloud 一样, 在项目初始化的时候, 保证先从配置中心拉取配置。完成之后才可以正常启动

springboot 配置文件优先级: bootstrap > application

这里的优先级是指读取的优先级, 配置内容是低优先级覆盖高优先级

Nacos 配置管理 dataId格式:

1
${prefix}-${spring.profile.actice}.${file-extension}
  • prefix 默认为 spring.application.name值, 也可通过spring.cloud.nacos.config.prefix配置
  • spring.profiles.active 即为当前环境对应的 profile,可参考 Spring Boot文档。 
    • 注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
  • file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml 类型。

配置客户端

  • 依赖引入:

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  • 搭建client

  • bootstrap yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # nacos配置
    server:
    port: 3377

    spring:
    application:
    name: nacos-config-client
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 #Nacos服务注册中心地址
    config:
    server-addr: localhost:8848 #Nacos作为配置中心地址
    file-extension: yaml #指定yaml格式的配置
    # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}

  • applicaton yaml

    1
    2
    3
    spring:
    profiles:
    active: dev # 表示开发环境

结合 application和 bootsrap 就会去nacos config 找到 dev版本的yaml 配置文件

  • 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    @EnableDiscoveryClient
    @SpringBootApplication
    public class NacosConfigClientMain3377
    {
    public static void main(String[] args) {
    SpringApplication.run(NacosConfigClientMain3377.class, args);
    }
    }
  • ConfigController

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    //使得当前类下的配置支持 Nacos 动态刷新功能
    @RefreshScope
    public class ConfigClientController {
    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo() {
    return configInfo;
    }
    }

config.info报错注意:

  1. boot 新版本休要手动引入spring-cloud-starter-bootstrap
  2. 检查bootstrap.yaml文件名是否写错了

手动跟新配置文件后, 动态刷新:

配置中心分类配置

  • 出现背景:多环境多项目管理
    • 一个系统会有 DEV PROD TEST环境

Namespace、Group和Service区别:

  • 默认下: Namespace = public , Group = DEFAUL_GROUP , 默认Cluster= DEFAULT
  • Namespace 主要用来实现隔离
    • 生产、开发、测试三个环境
  • Group 是可以把不同的微服务划分到同一个分组
  • Service 是微服务: 一个service 多个Cluster(集群)
  • Cluster: 对指定微服务的一个虚拟划分。为了容灾可以把Service 微服务分别部署在不同机房
  • Instance: 微服务的实例

DataId方案

主要是在 Nacos server 里面新建 test/prod 的配置文件。

然后通过更改 spring.profile.active 来进行修改。

Group 方案

  1. Nacos控制台创建DEV_GROUP
  2. 修改client端的bootstrap 和 application

  • 测试结果

Namespace

大差不差。

  1. Nacos 控制台新建命名空间
  2. 选中命名空间
  3. 新建配置文件
  4. 配置yaml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring:  
    application:
    name: nacos-config-client
    cloud:
    nacos:
    discovery:
    server-addr: 127.0.0.1:8848
    config:
    server-addr: localhost:8848
    file-extension: yaml
    namespace: xxxxxx--xxxxx---xxxxx ## 配置选则命名空间的id
    group: DEV_GROUP

集群和持久化配置(Important)

  • 前提条件
    • Linux
    • MySQL 主从模式 或者高可用数据库
      • 默认情况下 Nacos使用了嵌入式数据库derby
        • 集群化部署 存在一致性问题
        • 仅仅支持MySQL的集中式存储

集群架构图:

  • VIP: Virtual IP 虚拟IP 通过NGINX集群 反向代理 -> Nacos集群

  • Mysql 设置步骤:

  1. 安装数据库 5.65+
  2. 初始化数据库,初始化文件: nacos-mysql.sql
  3. 修改 nacos 的conf/application.yaml , 增加支持mysql数据源配置
  4. 添加mysql数据源url , 用户名与密码
  5. 再次已单机模式运行 nacos 转移数据
    1. start.sh -m standalone
  6. 配置Cluster
  7. ./start.sh -p xxxx 以xxxx端口启动

配置集群文件:

  • 配置 nacos/conf/Cluster

    1
    2
    3
    4
    # ip:port
    200.8.9.16:8848
    200.8.9.17:8848
    200.8.9.18:8848
  • Nginx Conf 负载均衡

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    upstream nacos{
    server1 200.8.9.16:8848;
    server2 200.8.9.17:8848;
    server3 200.8.9.18:8848;
    }


    server {
    listen 1111;
    proxy_pass http://nacos/
    }

Sentinel

  • 分布式流量来的防卫兵(Hystrix alibaba翻版)

Hystrix:

  • 手工搭建监控平台
  • 没有Web界面 进行细颗粒度的配置流量控制、速率控制、服务熔断…

Sentinel:

  • 单独组件
  • 界面化细颗粒度
  • 目标尽量配置、少少写代码

组成部分:

  • 核心库(Java客户端): 不依赖于任何框架,可以运行在所有Java环境。 对Dubbo和Spring Cloud 支持良好
  • 控制台: 基于SpringBoot开发, 打包后直接运行。

Dashborad:

  • 默认用户: sentinel
  • 密码: sentinel

How to Use?

  1. 运行Nacos server
  2. 新建cloudalibab-sentinel-service8401模块
    1. Pom
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      <dependencies>
      <!--SpringCloud ailibaba nacos -->
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>
      <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
      <dependency>
      <groupId>com.alibaba.csp</groupId>
      <artifactId>sentinel-datasource-nacos</artifactId>
      </dependency>
      <!--SpringCloud ailibaba sentinel -->
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
      </dependency>
      <!--openfeign-->
      <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
      </dependency>
      <!-- SpringBoot整合Web组件+actuator -->
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      <!--日常通用jar包配置-->
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
      </dependency>
      <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>4.6.3</version>
      </dependency>
      <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
      </dependency>
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      </dependency>

      </dependencies>
    2. Yml
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      server:
      port: 8401

      spring:
      application:
      name: cloudalibaba-sentinel-service
      cloud:
      nacos:
      discovery:
      #Nacos服务注册中心地址
      server-addr: localhost:8848
      sentinel:
      transport:
      #配置Sentinel dashboard地址
      dashboard: localhost:8080
      #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
      port: 8719

      management:
      endpoints:
      web:
      exposure:
      include: '*'
    3. 主启动类
      1
      2
      3
      4
      5
      6
      7
      8
      @EnableDiscoveryClient
      @SpringBootApplication
      public class SentinelMain8401 {
      public static void main(String[] args) {
      SpringApplication.run(SentinelMain8401.class, args);
      }
      }

    4. 业务类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12

      @RestController
      public class FlowLimitController {
      @GetMapping("/testA")
      public String testA(){
      return "I am test A";
      }
      @GetMapping("/testB")
      public String testb(){
      return "I am test B";
      }
      }
  3. 启动sentinel server
  4. 启动微服务

Tips:

  1. Sentinel 是懒加载, 服务启动时不会立马显示
  2. 执行一次他的接口之后才会添加

Sentinel 流控

  • 资源名: 默认路径名称

  • 针对来源: 针对调用者限流, 填写微服务名

  • 阈值类型:

    • QPS(每秒请求数量):QPS达到阈值限流
    • 线程数: 调用api的线程数达到阈值进行限流。
  • 集群模式?:不需要

  • 流控模式:

    • 直接: api达到限流条件直接限流

    • 关联: 关联资源达到阈值 限流自己

      • B关联的A资源到达阈值,限流A自己
    • 链路了只记录特定链路的流量(指定资源从入口资源进入的流量达到阈值)。,

  • 流控效果

    • 快速失败: 直接失败抛异常

      • 要配置全局异常
    • Warm up: 根据Code Factor冷加载因子,从阈值/codeFactor 经过预热时长,达到阈值QPS

      • 公式: 阈值 / coldFactor(默认3) 经过要预热时长后才可以达到阈值。
      • 案例: 秒杀系统突然瞬间会有很大流量,预热可以慢慢把流量放进来,慢慢增长阈值。
    • 排队等待: 匀速排队,让请求匀速通过, 阈值的类型必须设置为QPS否则无效

熔断降级

  • Tips: 熔断会降级,但是降级不一定熔断。

    • 熔断:在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
    • 降级:当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心业务正常运作或高效运作。
      • 就是尽量把资源给核心
    • 服务降级和熔断:
      • 相同点:
        • 目标相同: 可用性和可靠性
        • 用户体验:某些功能不能用
      • 不同:
        • 熔断一般是下游故障引起的
        • 服务降级是整体负荷
  • RT(老版本): 平均响应时间

    • RT超出阈值且时间窗口内请求 >= 5 触发降级

    • 窗口期过后关闭断路器

    • RT最大4900

  • 慢调用比例:设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

  • 异常比例数(秒级别):QPS >= 5 且超过阈值触发降级, 时间窗口结束关闭降级。

  • 异常数(分钟): 异常数超过阈值,触发降级,时间窗口结束关闭降级

  • 半开状态:新版本有了

    • 自动检测是否有请求异常
      • 没有: 关闭断路器
      • 有:持续

自适应保护

  • 目的

    • 保证系统可靠 不跨
    • 稳定前提下、保证吞吐量
  • 原自适应保护根据硬指标->负载(Load)

    • 缺点:
      • 根据负载Load调整流量,有延迟。
      • 恢复慢,下游不可靠可能会导致RT很高
  • TCP BBR算法启发

    • [[计网#拥塞控制]]
  • 系统规则:

    • Load (Unix-like 有效):load1超阈值且并发线程超过系统容量触发保护
      • 系统容量: maxQPS * minRt
    • cpu usage
    • RT
    • 线程数
    • 入口QPS

热点Key限流(重要且实用)

  • 热点: 频繁访问的数据
    • 举例子: 商品Id, 一段时间内某个商品爆火
  • 热点参数会统计传入参数中的热点参数,根据限流方式进行限流。
  • http://xxxxxx?param=qqq, qqq 就是参数

@SentinelResouce : 处理的 Sentinel 控制台的违规,而不是 Java 的

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/hotKeyTest")
//下面的注解value不需要和GetMapping的值一样,符合Sentinel的Key就好
// blockHandler 是不符合热点规则的处理方法
@SentinelResource(value = "hotKeyTest", blockHandler = "howDareU")
public String hotKeyTest(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){

return "THIS IS A HOT KEY";
}

public String howDareU(String p1, String p2, BlockException exception){
return "how dare you break the hot key law? Fuck u!";
}

  • 参数索引: 第几个参数
  1. value不需要和GetMapping的值一样,符合Sentinel的Key就好, 唯一
  2. blockHandler 是不符合热点规则的处理方法

测试结果:

参数例外项:

  1. 期望P1 参数特殊值是,限流值变化
    • 只支持JAVA 八种基本类型

p1=”1”时:

p1=”2”

Resouces配置

引入之前自己的请求包。

资源名称限流

主要还是:@SentinelResouce(value="资源名", blockHandler="兜底方法名")

URL 限流

@XXXMAPPING("url")
@SentinelResouce(value="资源名")

流量控制选择url

上述方法的核心问题:没有全局处理兜底方法。

自定义限流逻辑

  1. 自定义限流类
    1
    2
    3
    4
    5
    6
    public class CustomerBlockHandler
    {
    public static CommonResult handleException(BlockException exception){
    return new CommonResult(2020,"自定义的限流处理信息......CustomerBlockHandler");
    }
    }
  2. 自定义限流处理逻辑
    1
    2
    3
    4
    5
    6
    7
    @GetMapping("/rateLimit/customerBlockHandler")
    @SentinelResource(value = "customerBlockHandler",
    blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
    public CommonResult customerBlockHandler()
    {
    return new CommonResult(200,"按客户自定义限流处理逻辑");
    }

持久化

将限流配置持久化进入Nacos。

  1. 持久化包以来

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
  2. YAML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    server:
    port: 8401

    spring:
    application:
    name: cloudalibaba-sentinel-service
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 #Nacos服务注册中心地址
    sentinel:
    transport:
    dashboard: localhost:8080 #配置Sentinel dashboard地址
    port: 8719
    datasource:
    ds1:
    nacos:
    server-addr: localhost:8848
    dataId: cloudalibaba-sentinel-service
    groupId: DEFAULT_GROUP
    data-type: json
    rule-type: flow

    management:
    endpoints:
    web:
    exposure:
    include: '*'

    feign:
    sentinel:
    enabled: true # 激活Sentinel对Feign的支持
  3. nacos 新建配置

配置解读:

1
2
3
4
5
6
7
8
9
10
11
12
13
resource:资源名称;

limitApp:来源应用;

grade:阈值类型,0表示线程数,1表示QPS;

count:单机阈值;

strategy:流控模式,0表示直接,1表示关联,2表示链路;

controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;

clusterMode:是否集群。
  1. 规则出现

//Todo

Seate

依托大便